diff --git a/crates/ringo-phone/src/app.rs b/crates/ringo-phone/src/app.rs index 1b7579e..866f949 100644 --- a/crates/ringo-phone/src/app.rs +++ b/crates/ringo-phone/src/app.rs @@ -288,9 +288,77 @@ fn build_subtitle(profile: &profile::Profile, fields: &[String]) -> String { "outbound" => profile.outbound.clone().filter(|s| !s.is_empty()), "stun_server" => profile.stun_server.clone().filter(|s| !s.is_empty()), "media_enc" => Some(profile.media_enc.as_deref().unwrap_or("none").to_string()), - _ => None, + // Anything the user stored under [metadata] in the profile, e.g. + // `metadata.reachable_at`. The profile carries that map already and + // the form preserves it; this is the first place it is shown. + k => k + .strip_prefix("metadata.") + .and_then(|key| profile.metadata.get(key)) + .cloned(), }) .filter(|s| !s.is_empty()) .collect::>() .join(" · ") } + +#[cfg(test)] +mod tests { + use super::build_subtitle; + use crate::profile::Profile; + + fn profile_with_metadata(pairs: &[(&str, &str)]) -> Profile { + let mut p = Profile { + username: "1125452e0".into(), + domain: "sipgate.de".into(), + ..Default::default() + }; + p.metadata = pairs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + p + } + + fn fields(list: &[&str]) -> Vec { + list.iter().map(|s| s.to_string()).collect() + } + + #[test] + fn metadata_keys_reach_the_subtitle() { + // The registration username is a device id, not something anyone dials. + // A profile can say how it is actually reached, and this is where that + // shows up. + let p = profile_with_metadata(&[("reachable_at", "11")]); + assert_eq!( + build_subtitle(&p, &fields(&["aor", "metadata.reachable_at"])), + "sip:1125452e0@sipgate.de · 11" + ); + } + + #[test] + fn an_absent_metadata_key_is_skipped_not_blank() { + let p = profile_with_metadata(&[("reachable_at", "11")]); + assert_eq!( + build_subtitle(&p, &fields(&["metadata.nope", "metadata.reachable_at"])), + "11", + "a missing key must not leave a stray separator" + ); + } + + #[test] + fn an_unknown_field_is_still_ignored() { + // The metadata arm replaced the catch-all, so this has to keep working. + let p = profile_with_metadata(&[]); + assert_eq!( + build_subtitle(&p, &fields(&["nonsense", "domain"])), + "sipgate.de" + ); + } + + #[test] + fn metadata_is_not_confused_with_a_field_of_its_own_name() { + // Bare `metadata` names no key and must resolve to nothing. + let p = profile_with_metadata(&[("reachable_at", "11")]); + assert_eq!(build_subtitle(&p, &fields(&["metadata"])), ""); + } +} diff --git a/crates/ringo-phone/src/config.rs b/crates/ringo-phone/src/config.rs index bb3e490..db088b5 100644 --- a/crates/ringo-phone/src/config.rs +++ b/crates/ringo-phone/src/config.rs @@ -193,7 +193,9 @@ pub fn sounds_dir() -> Option { pub struct PickerConfig { /// Profile fields shown as subtitle next to each entry. /// Available: aor, username, domain, display_name, transport, - /// auth_user, outbound, stun_server, media_enc + /// auth_user, outbound, stun_server, media_enc, notes, + /// and `metadata.` for anything under the profile's + /// `[metadata]` table. pub info: Vec, } diff --git a/crates/ringo-phone/src/profile.rs b/crates/ringo-phone/src/profile.rs index 13467b3..0a6ccc5 100644 --- a/crates/ringo-phone/src/profile.rs +++ b/crates/ringo-phone/src/profile.rs @@ -57,6 +57,10 @@ pub struct Profile { pub deflect: bool, #[serde(default)] pub deflect_target: Option, + /// Free-form key/value pairs about this profile, never interpreted by + /// ringo. Surfaced through the picker's subtitle as `metadata.` (see + /// `picker.info`), for whatever tells profiles apart that the SIP fields do + /// not — environment, tenant, the extension it answers on. #[serde(default)] pub metadata: HashMap, } diff --git a/docs/src/ringo-phone/configuration.md b/docs/src/ringo-phone/configuration.md index 92c11fa..a7cb083 100644 --- a/docs/src/ringo-phone/configuration.md +++ b/docs/src/ringo-phone/configuration.md @@ -56,10 +56,31 @@ phone on the next start, and nothing hand-written gets overwritten. ```toml [picker] # Fields shown next to each profile name in the picker. Available: aor, username, -# domain, display_name, transport, auth_user, outbound, stun_server, media_enc. +# domain, display_name, transport, auth_user, outbound, stun_server, media_enc, +# notes, and metadata. (see below). info = ["aor"] # default ``` +A profile can carry free-form key/value pairs that ringo never interprets, and +the picker can show them: + +```toml +# ~/.config/ringo/profiles//profile.toml +[metadata] +env = "staging" +``` + +```toml +# ~/.config/ringo/ringo.toml +[picker] +info = ["aor", "metadata.env"] +``` + +This is for whatever tells your profiles apart that the SIP fields do not — the +environment an account belongs to, the tenant it serves, the extension it +answers on. A key that a profile does not set is simply left out, so profiles +can carry different ones. + ## Theme All UI colors are configurable — named values or `#rrggbb` hex.