Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion crates/ringo-phone/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
.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<String> {
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"])), "");
}
}
4 changes: 3 additions & 1 deletion crates/ringo-phone/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ pub fn sounds_dir() -> Option<PathBuf> {
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.<key>` for anything under the profile's
/// `[metadata]` table.
pub info: Vec<String>,
}

Expand Down
4 changes: 4 additions & 0 deletions crates/ringo-phone/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ pub struct Profile {
pub deflect: bool,
#[serde(default)]
pub deflect_target: Option<String>,
/// Free-form key/value pairs about this profile, never interpreted by
/// ringo. Surfaced through the picker's subtitle as `metadata.<key>` (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<String, String>,
}
Expand Down
23 changes: 22 additions & 1 deletion docs/src/ringo-phone/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<key> (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/<name>/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.
Expand Down
Loading