diff --git a/crates/solstone-linux/build.rs b/crates/solstone-linux/build.rs index 1798107..87acbad 100644 --- a/crates/solstone-linux/build.rs +++ b/crates/solstone-linux/build.rs @@ -9,10 +9,12 @@ fn main() { let out = PathBuf::from(env::var_os("OUT_DIR").unwrap_or_default()).join("tray_icons.rs"); let mut generated = String::new(); for (constant, file) in [ - ("RECORDING", "solstone-recording.svg"), + ("HEALTHY", "solstone-recording.svg"), + ("ATTENTION", "solstone-attention.svg"), ("PAUSED", "solstone-paused.svg"), - ("SYNCING", "solstone-syncing.svg"), + ("OFFLINE", "solstone-offline.svg"), ("ERROR", "solstone-error.svg"), + ("CONNECTING", "solstone-syncing.svg"), ] { let path = root.join(file); println!("cargo:rerun-if-changed={}", path.display()); diff --git a/crates/solstone-linux/src/cli.rs b/crates/solstone-linux/src/cli.rs index be25aba..1a588fa 100644 --- a/crates/solstone-linux/src/cli.rs +++ b/crates/solstone-linux/src/cli.rs @@ -1250,7 +1250,7 @@ mod tests { 0 ); let expected = format!( - "Config: {}\nJournal link: managed privately\nStream: test-stream\n\nCache: {}\n 0 segments across 0 day(s), 0.0 MB\nRetain: 7 day(s)\nSync: offline — saving locally; will retry; pending unconfirmed\n\nService: active\n", + "Config: {}\nJournal link: managed privately\nStream: test-stream\n\nCache: {}\n 0 segments across 0 day(s), 0.0 MB\nRetain: 7 day(s)\nSync: offline; held on this device; will retry\n\nService: active\n", config.config_path().display(), config.captures_dir().display() ); diff --git a/crates/solstone-linux/src/sync.rs b/crates/solstone-linux/src/sync.rs index 034e2cb..fff768d 100644 --- a/crates/solstone-linux/src/sync.rs +++ b/crates/solstone-linux/src/sync.rs @@ -2085,10 +2085,10 @@ mod tests { }), ..SyncFacts::default() }, - "on — connected", + "on, connected", "Active", "connected", - "Sync: connected — up to date (0 pending)", + "Sync: connected; up to date", "ok", ), ( @@ -2097,10 +2097,10 @@ mod tests { last_error_code: Some(404), ..SyncFacts::default() }, - "on — update required", + "on, update required", "NeedsAttention", "update-required", - "Sync: update required — update solstone-linux; pending unconfirmed", + "Sync: update required; update the solstone app", "fail", ), ]; @@ -2175,17 +2175,17 @@ mod tests { assert_eq!(model.sni_status, sampled.sni_status); assert_eq!( sampled.cli, - "Sync: connection unavailable — saving locally; restart sol; if this continues, pair this device again" + "Sync: connection unavailable; held on this device; restart the solstone app; if this continues, pair this device again" ); assert_eq!(sampled.doctor_severity, "fail"); assert_eq!( sampled.doctor_detail, - "sync health: connection unavailable; restart sol; if this continues, pair this device again" + "sync health: connection unavailable; restart the solstone app; if this continues, pair this device again" ); assert_eq!(sampled.dbus, "transport-unavailable"); assert_eq!( sampled.accessible_recording, - "sol — on, connection unavailable, saving locally" + "on, connection unavailable, held on this device" ); service.shutdown(Duration::from_secs(1)).await.unwrap(); @@ -2470,13 +2470,10 @@ mod tests { crate::sync_health::HealthState::UpdateRequired ); assert_eq!(health.pending_display, "pending unconfirmed"); - assert_eq!(health.header_recording, "on — update required"); + assert_eq!(health.header_recording, "on, update required"); assert_eq!(health.sni_status, "NeedsAttention"); assert_eq!(health.dbus, "update-required"); - assert_eq!( - health.cli, - "Sync: update required — update solstone-linux; pending unconfirmed" - ); + assert_eq!(health.cli, "Sync: update required; update the solstone app"); assert_eq!(health.doctor_severity, "fail"); } diff --git a/crates/solstone-linux/src/sync_health.rs b/crates/solstone-linux/src/sync_health.rs index 49f2bde..b56b0d4 100644 --- a/crates/solstone-linux/src/sync_health.rs +++ b/crates/solstone-linux/src/sync_health.rs @@ -144,32 +144,32 @@ pub static SURFACE_BY_STATE: LazyLock> = Laz ( HealthState::UnsafeLinkState, HealthSurface { - header_recording: "on — private state unsafe", - header_idle: "idle — private state unsafe", - sync_line: "sync: private state unsafe", - tooltip: "sync: private state unsafe; repair private state and restart sol", - accessible_recording: "sol — on, private state unsafe", - accessible_idle: "sol — idle, private state unsafe", - icon: "error", + header_recording: "on, pairing unsafe", + header_idle: "idle, pairing unsafe", + sync_line: "sync: pairing unsafe", + tooltip: "sync: pairing unsafe; repair this device's pairing and restart the solstone app", + accessible_recording: "on, pairing unsafe", + accessible_idle: "idle, pairing unsafe", + icon: "attention", sni: "NeedsAttention", - cli: "Sync: private state unsafe — repair private state and restart sol; pending unconfirmed", + cli: "Sync: pairing unsafe; repair this device's pairing and restart the solstone app", doctor_severity: "fail", - doctor_detail: "sync health: private state unsafe; repair private state and restart sol", + doctor_detail: "sync health: pairing unsafe; repair this device's pairing and restart the solstone app", dbus: "unsafe-link-state", }, ), ( HealthState::RePairRequired, HealthSurface { - header_recording: "on — pair again", - header_idle: "idle — pair again", + header_recording: "on, pair again", + header_idle: "idle, pair again", sync_line: "sync: pair again", tooltip: "sync: pair this device with your journal again", - accessible_recording: "sol — on, pair again", - accessible_idle: "sol — idle, pair again", - icon: "error", + accessible_recording: "on, pair again", + accessible_idle: "idle, pair again", + icon: "attention", sni: "NeedsAttention", - cli: "Sync: pair again — pair this device with your journal again; pending unconfirmed", + cli: "Sync: pair again; pair this device with your journal again", doctor_severity: "fail", doctor_detail: "sync health: pair this device with your journal again", dbus: "re-pair-required", @@ -178,32 +178,32 @@ pub static SURFACE_BY_STATE: LazyLock> = Laz ( HealthState::TokenPersistenceFailed, HealthSurface { - header_recording: "on — private state not saved", - header_idle: "idle — private state not saved", - sync_line: "sync: private state not saved", - tooltip: "sync: private state not saved; fix private state permissions, then restart sol", - accessible_recording: "sol — on, private state not saved", - accessible_idle: "sol — idle, private state not saved", + header_recording: "on, pairing not saved", + header_idle: "idle, pairing not saved", + sync_line: "sync: pairing not saved", + tooltip: "sync: pairing not saved; fix this device's pairing permissions, then restart the solstone app", + accessible_recording: "on, pairing not saved", + accessible_idle: "idle, pairing not saved", icon: "error", sni: "NeedsAttention", - cli: "Sync: private state not saved — fix private state permissions, then restart sol; pending unconfirmed", + cli: "Sync: pairing not saved; fix this device's pairing permissions, then restart the solstone app", doctor_severity: "fail", - doctor_detail: "sync health: private state not saved; fix private state permissions, then restart sol", + doctor_detail: "sync health: pairing not saved; fix this device's pairing permissions, then restart the solstone app", dbus: "token-persistence-failed", }, ), ( HealthState::PairingRequired, HealthSurface { - header_recording: "on — pairing required", - header_idle: "idle — pairing required", + header_recording: "on, pairing required", + header_idle: "idle, pairing required", sync_line: "sync: pairing required", tooltip: "sync: pair this device with your journal", - accessible_recording: "sol — on, pairing required", - accessible_idle: "sol — idle, pairing required", - icon: "error", + accessible_recording: "on, pairing required", + accessible_idle: "idle, pairing required", + icon: "attention", sni: "NeedsAttention", - cli: "Sync: pairing required — pair this device with your journal; pending unconfirmed", + cli: "Sync: pairing required; pair this device with your journal", doctor_severity: "fail", doctor_detail: "sync health: pair this device with your journal", dbus: "pairing-required", @@ -212,136 +212,136 @@ pub static SURFACE_BY_STATE: LazyLock> = Laz ( HealthState::UpdateRequired, HealthSurface { - header_recording: "on — update required", - header_idle: "idle — update required", - sync_line: "sync: update solstone-linux", - tooltip: "sync: update required; update solstone-linux", - accessible_recording: "sol — on, update required", - accessible_idle: "sol — idle, update required", - icon: "error", + header_recording: "on, update required", + header_idle: "idle, update required", + sync_line: "sync: update the solstone app", + tooltip: "sync: update required; update the solstone app", + accessible_recording: "on, update required", + accessible_idle: "idle, update required", + icon: "attention", sni: "NeedsAttention", - cli: "Sync: update required — update solstone-linux; pending unconfirmed", + cli: "Sync: update required; update the solstone app", doctor_severity: "fail", - doctor_detail: "sync health: update required; update solstone-linux", + doctor_detail: "sync health: update required; update the solstone app", dbus: "update-required", }, ), ( HealthState::TransportUnavailable, HealthSurface { - header_recording: "on — connection unavailable (saving locally)", - header_idle: "idle — connection unavailable (saving locally)", - sync_line: "sync: connection unavailable; saving locally", - tooltip: "sync: connection unavailable; restart sol; if this continues, pair this device again", - accessible_recording: "sol — on, connection unavailable, saving locally", - accessible_idle: "sol — idle, connection unavailable, saving locally", - icon: "error", + header_recording: "on, connection unavailable (held on this device)", + header_idle: "idle, connection unavailable (held on this device)", + sync_line: "sync: connection unavailable; held on this device", + tooltip: "sync: connection unavailable; restart the solstone app; if this continues, pair this device again", + accessible_recording: "on, connection unavailable, held on this device", + accessible_idle: "idle, connection unavailable, held on this device", + icon: "offline", sni: "NeedsAttention", - cli: "Sync: connection unavailable — saving locally; restart sol; if this continues, pair this device again", + cli: "Sync: connection unavailable; held on this device; restart the solstone app; if this continues, pair this device again", doctor_severity: "fail", - doctor_detail: "sync health: connection unavailable; restart sol; if this continues, pair this device again", + doctor_detail: "sync health: connection unavailable; restart the solstone app; if this continues, pair this device again", dbus: "transport-unavailable", }, ), ( HealthState::Offline, HealthSurface { - header_recording: "on — offline (saving locally)", - header_idle: "idle — offline (saving locally)", + header_recording: "on, offline (held on this device)", + header_idle: "idle, offline (held on this device)", sync_line: "sync: offline; will retry", - tooltip: "sync: offline; saving locally; will retry", - accessible_recording: "sol — on, offline, saving locally", - accessible_idle: "sol — idle, offline, saving locally", - icon: "syncing", + tooltip: "sync: offline; held on this device; will retry", + accessible_recording: "on, offline, held on this device", + accessible_idle: "idle, offline, held on this device", + icon: "offline", sni: "Active", - cli: "Sync: offline — saving locally; will retry; pending unconfirmed", + cli: "Sync: offline; held on this device; will retry", doctor_severity: "warn", - doctor_detail: "sync health: offline; saving locally; will retry", + doctor_detail: "sync health: offline; held on this device; will retry", dbus: "offline", }, ), ( HealthState::ListenerReady, HealthSurface { - header_recording: "on — confirming device", - header_idle: "idle — confirming device", - sync_line: "sync: confirming device", - tooltip: "sync: wait while sol confirms this device with your journal", - accessible_recording: "sol — on, confirming device", - accessible_idle: "sol — idle, confirming device", - icon: "syncing", + header_recording: "on, confirming with your journal", + header_idle: "idle, confirming with your journal", + sync_line: "sync: confirming with your journal", + tooltip: "sync: wait while this device confirms with your journal", + accessible_recording: "on, confirming with your journal", + accessible_idle: "idle, confirming with your journal", + icon: "connecting", sni: "Active", - cli: "Sync: confirming device — wait while sol confirms this device with your journal; pending unconfirmed", + cli: "Sync: wait while this device confirms with your journal", doctor_severity: "warn", - doctor_detail: "sync health: wait while sol confirms this device with your journal", + doctor_detail: "sync health: wait while this device confirms with your journal", dbus: "listener-ready", }, ), ( HealthState::NotReported, HealthSurface { - header_recording: "on — not reported", - header_idle: "idle — not reported", - sync_line: "sync: not reported", - tooltip: "sync: sol is not reporting right now", - accessible_recording: "on, sync not reported", - accessible_idle: "idle, sync not reported", - icon: "syncing", + header_recording: "on, no status", + header_idle: "idle, no status", + sync_line: "sync: no status", + tooltip: "sync: no status from the solstone app right now", + accessible_recording: "on, no status", + accessible_idle: "idle, no status", + icon: "offline", sni: "Active", - cli: "Sync: not reported — sol is not running, or has not reported yet", + cli: "Sync: no status; the solstone app is not running, or has no status yet", doctor_severity: "warn", - doctor_detail: "sol is not running, or has not reported yet", + doctor_detail: "the solstone app is not running, or has no status yet", dbus: "not-reported", }, ), ( HealthState::Connecting, HealthSurface { - header_recording: "on — connecting", - header_idle: "idle — connecting", - sync_line: "sync: checking...", - tooltip: "sync: wait while sol connects to your journal", - accessible_recording: "sol — on, connecting", - accessible_idle: "sol — idle, connecting", - icon: "syncing", + header_recording: "on, connecting", + header_idle: "idle, connecting", + sync_line: "sync: connecting", + tooltip: "sync: wait while this device connects to your journal", + accessible_recording: "on, connecting", + accessible_idle: "idle, connecting", + icon: "connecting", sni: "Active", - cli: "Sync: connecting — wait while sol connects to your journal; pending unconfirmed", + cli: "Sync: connecting; wait while this device connects to your journal", doctor_severity: "warn", - doctor_detail: "sync health: connecting; wait while sol connects to your journal", + doctor_detail: "sync health: connecting; wait while this device connects to your journal", dbus: "connecting", }, ), ( HealthState::Syncing, HealthSurface { - header_recording: "on — syncing", - header_idle: "idle — syncing", + header_recording: "on, syncing", + header_idle: "idle, syncing", sync_line: "sync: {progress}", tooltip: "sync: {progress}", - accessible_recording: "sol — on, syncing", - accessible_idle: "sol — idle, syncing", - icon: "syncing", + accessible_recording: "on, syncing", + accessible_idle: "idle, syncing", + icon: "healthy", sni: "Active", - cli: "Sync: syncing — pending unconfirmed until this pass finishes", + cli: "Sync: syncing; your journal is receiving; not confirmed yet", doctor_severity: "ok", - doctor_detail: "sync health: sync pass active; pending unconfirmed until check completes", + doctor_detail: "sync health: your journal is receiving; not confirmed yet", dbus: "syncing", }, ), ( HealthState::Connected, HealthSurface { - header_recording: "on — connected", - header_idle: "idle — connected", + header_recording: "on, connected", + header_idle: "idle, connected", sync_line: "sync: up to date", tooltip: "sync: up to date", - accessible_recording: "sol — on, sync up to date", - accessible_idle: "sol — idle, sync up to date", - icon: "recording", + accessible_recording: "on, sync up to date", + accessible_idle: "idle, sync up to date", + icon: "healthy", sni: "Active", - cli: "Sync: connected — up to date (0 pending)", + cli: "Sync: connected; up to date", doctor_severity: "ok", - doctor_detail: "sync health: up to date; 0 pending confirmed at {sync_ts}", + doctor_detail: "sync health: up to date at {sync_ts}", dbus: "connected", }, ), @@ -637,22 +637,25 @@ mod tests { DEFAULT_SYNC_STALE_THRESHOLD as f64, ); assert_eq!(health.dbus, "not-reported"); - assert_eq!(health.header_recording, "on — not reported"); - assert_eq!(health.header_idle, "idle — not reported"); - assert_eq!(health.sync_line, "sync: not reported"); - assert_eq!(health.tooltip, "sync: sol is not reporting right now"); - assert_eq!(health.accessible_recording, "on, sync not reported"); - assert_eq!(health.accessible_idle, "idle, sync not reported"); - assert_eq!(health.icon, "syncing"); + assert_eq!(health.header_recording, "on, no status"); + assert_eq!(health.header_idle, "idle, no status"); + assert_eq!(health.sync_line, "sync: no status"); + assert_eq!( + health.tooltip, + "sync: no status from the solstone app right now" + ); + assert_eq!(health.accessible_recording, "on, no status"); + assert_eq!(health.accessible_idle, "idle, no status"); + assert_eq!(health.icon, "offline"); assert_eq!(health.sni_status, "Active"); assert_eq!( health.cli, - "Sync: not reported — sol is not running, or has not reported yet" + "Sync: no status; the solstone app is not running, or has no status yet" ); assert_eq!(health.doctor_severity, "warn"); assert_eq!( health.doctor_detail, - "sol is not running, or has not reported yet" + "the solstone app is not running, or has no status yet" ); } @@ -759,7 +762,7 @@ mod tests { let health = derive_health(&facts, 1000.0, DEFAULT_SYNC_STALE_THRESHOLD as f64); assert_eq!(health.state, HealthState::Offline); assert_eq!(health.sni_status, "Active"); - assert!(health.tooltip.contains("saving locally")); + assert!(health.tooltip.contains("held on this device")); } // Connected requires both zero pending custody and positive linked-transport evidence. diff --git a/crates/solstone-linux/src/tray.rs b/crates/solstone-linux/src/tray.rs index b836333..43a76a5 100644 --- a/crates/solstone-linux/src/tray.rs +++ b/crates/solstone-linux/src/tray.rs @@ -62,10 +62,13 @@ impl Tray for KsniTray { } fn icon_pixmap(&self) -> Vec { let data = match self.model.icon.as_str() { - "error" | "stopped" => generated::ERROR, - "syncing" => generated::SYNCING, - "paused" | "idle" => generated::PAUSED, - _ => generated::RECORDING, + "healthy" => generated::HEALTHY, + "attention" => generated::ATTENTION, + "paused" => generated::PAUSED, + "offline" => generated::OFFLINE, + "error" => generated::ERROR, + "connecting" => generated::CONNECTING, + _ => generated::OFFLINE, }; vec![Icon { width: 64, @@ -186,8 +189,9 @@ mod tests { use super::*; use crate::{ observer::{Mode, StateSnapshot}, - sync_health::{SyncFacts, derive_health}, - tray_model, + private_link::LinkFactState, + sync_health::{ErrorType, SyncFacts, SyncHealth, derive_health}, + tray_model::{self, TrayStatus}, }; fn tray() -> KsniTray { let snapshot = StateSnapshot { @@ -316,6 +320,228 @@ mod tests { assert_eq!(open_journal.enabled, expected); } } + + fn pixmap_from_health(health: &SyncHealth, status: TrayStatus) -> Vec { + let mut tray = tray(); + tray.model.icon = tray_model::icon_name(status, health); + tray.icon_pixmap() + .into_iter() + .next() + .expect("one pixmap") + .data + } + + fn pixmap_for(facts: &SyncFacts, status: TrayStatus) -> Vec { + pixmap_from_health(&derive_health(facts, 1_000.0, 600.0), status) + } + + #[test] + fn recording_health_states_select_embedded_mark_pixmaps() { + let cases = [ + ( + SyncFacts { + link: Some(LinkFactState { + private_state_invalid: true, + ..Default::default() + }), + ..Default::default() + }, + generated::ATTENTION, + ), + ( + SyncFacts { + last_error_class: Some(ErrorType::Auth), + ..Default::default() + }, + generated::ATTENTION, + ), + ( + SyncFacts { + link: Some(LinkFactState { + pairing_required: true, + ..Default::default() + }), + ..Default::default() + }, + generated::ATTENTION, + ), + ( + SyncFacts { + last_error_class: Some(ErrorType::Incompatible), + ..Default::default() + }, + generated::ATTENTION, + ), + ( + SyncFacts { + link: Some(LinkFactState { + token_persistence_failure: true, + ..Default::default() + }), + ..Default::default() + }, + generated::ERROR, + ), + ( + SyncFacts { + link: Some(LinkFactState { + transport_unavailable: true, + ..Default::default() + }), + ..Default::default() + }, + generated::OFFLINE, + ), + ( + SyncFacts { + last_error_class: Some(ErrorType::Transient), + ..Default::default() + }, + generated::OFFLINE, + ), + (SyncFacts::default(), generated::OFFLINE), + ( + SyncFacts { + link: Some(LinkFactState { + listener_ready: true, + ..Default::default() + }), + ..Default::default() + }, + generated::CONNECTING, + ), + ( + SyncFacts { + link: Some(LinkFactState { + carrier_proven: true, + ..Default::default() + }), + ..Default::default() + }, + generated::CONNECTING, + ), + ( + SyncFacts { + in_progress: true, + link: Some(LinkFactState { + observer_registered: true, + ..Default::default() + }), + ..Default::default() + }, + generated::HEALTHY, + ), + ( + SyncFacts { + pending_confirmed: Some(0), + link: Some(LinkFactState { + carrier_proven: true, + observer_registered: true, + ..LinkFactState::default() + }), + ..SyncFacts::default() + }, + generated::HEALTHY, + ), + ]; + for (facts, expected) in cases { + assert_eq!( + pixmap_for(&facts, TrayStatus::Recording).as_slice(), + expected + ); + } + } + + #[test] + fn capture_status_overlays_only_healthy_and_connecting() { + let connected = SyncFacts { + pending_confirmed: Some(0), + link: Some(LinkFactState { + carrier_proven: true, + observer_registered: true, + ..LinkFactState::default() + }), + ..SyncFacts::default() + }; + let syncing = SyncFacts { + in_progress: true, + link: Some(LinkFactState { + observer_registered: true, + ..Default::default() + }), + ..Default::default() + }; + let connecting = SyncFacts { + link: Some(LinkFactState { + carrier_proven: true, + ..Default::default() + }), + ..Default::default() + }; + let update_required = SyncFacts { + last_error_class: Some(ErrorType::Incompatible), + ..Default::default() + }; + let token_persistence_failed = SyncFacts { + link: Some(LinkFactState { + token_persistence_failure: true, + ..Default::default() + }), + ..Default::default() + }; + let offline = SyncFacts { + last_error_class: Some(ErrorType::Transient), + ..Default::default() + }; + let cases = [ + (TrayStatus::Idle, &connected, generated::HEALTHY), + (TrayStatus::Idle, &syncing, generated::HEALTHY), + (TrayStatus::Paused, &connected, generated::PAUSED), + (TrayStatus::Paused, &connecting, generated::PAUSED), + (TrayStatus::Paused, &update_required, generated::ATTENTION), + ( + TrayStatus::Paused, + &token_persistence_failed, + generated::ERROR, + ), + (TrayStatus::Paused, &offline, generated::OFFLINE), + (TrayStatus::Stopped, &connected, generated::PAUSED), + ( + TrayStatus::Stopped, + &token_persistence_failed, + generated::ERROR, + ), + ]; + for (status, facts, expected) in cases { + assert_eq!(pixmap_for(facts, status).as_slice(), expected); + } + } + + #[test] + fn embedded_pixmaps_are_pairwise_distinct() { + let marks = [ + generated::HEALTHY, + generated::ATTENTION, + generated::PAUSED, + generated::OFFLINE, + generated::ERROR, + generated::CONNECTING, + ]; + for (i, left) in marks.iter().enumerate() { + for right in &marks[i + 1..] { + assert_ne!(left, right); + } + } + } + + #[test] + fn unrecognized_mark_name_draws_offline_not_healthy() { + let mut health = derive_health(&SyncFacts::default(), 1_000.0, 600.0); + health.icon = "recording".into(); + let bytes = pixmap_from_health(&health, TrayStatus::Recording); + assert_eq!(bytes.as_slice(), generated::OFFLINE); + assert_ne!(bytes.as_slice(), generated::HEALTHY); + } } // Python tray provenance (35/35): diff --git a/crates/solstone-linux/src/tray_model.rs b/crates/solstone-linux/src/tray_model.rs index 5825eae..97f06a0 100644 --- a/crates/solstone-linux/src/tray_model.rs +++ b/crates/solstone-linux/src/tray_model.rs @@ -103,23 +103,20 @@ pub fn sni_status(status: TrayStatus, health: &SyncHealth) -> String { } pub fn icon_name(status: TrayStatus, health: &SyncHealth) -> String { - if health.icon == "error" { - "error" - } else if status == TrayStatus::Stopped { - "stopped" - } else if status == TrayStatus::Paused { - "paused" - } else if health.icon == "syncing" { - "syncing" - } else if status == TrayStatus::Idle - && health.state == crate::sync_health::HealthState::Connected - { - "idle" - } else { - match health.icon.as_str() { - "recording" | "paused" | "idle" | "stopped" | "syncing" | "error" => &health.icon, - _ => "recording", - } + match health.icon.as_str() { + "healthy" => match status { + TrayStatus::Paused | TrayStatus::Stopped => "paused", + TrayStatus::Recording | TrayStatus::Idle => "healthy", + }, + "connecting" => match status { + TrayStatus::Paused | TrayStatus::Stopped => "paused", + TrayStatus::Recording | TrayStatus::Idle => "connecting", + }, + "attention" => "attention", + "paused" => "paused", + "offline" => "offline", + "error" => "error", + _ => "offline", } .to_owned() } @@ -224,27 +221,24 @@ mod tests { ); assert_eq!( header_label(TrayStatus::Recording, &connected, 0), - "on — connected" + "on, connected" ); assert_eq!( header_label(TrayStatus::Recording, &syncing, 0), - "on — syncing" + "on, syncing" ); assert_eq!( header_label(TrayStatus::Recording, &offline, 0), - "on — offline (saving locally)" + "on, offline (held on this device)" ); assert_eq!( header_label(TrayStatus::Idle, &connected, 0), - "idle — connected" - ); - assert_eq!( - header_label(TrayStatus::Idle, &syncing, 0), - "idle — syncing" + "idle, connected" ); + assert_eq!(header_label(TrayStatus::Idle, &syncing, 0), "idle, syncing"); assert_eq!( header_label(TrayStatus::Idle, &offline, 0), - "idle — offline (saving locally)" + "idle, offline (held on this device)" ); assert_eq!(header_label(TrayStatus::Paused, &connected, 0), "paused"); assert_eq!( @@ -401,7 +395,7 @@ mod tests { 600.0, ); let m = build(&snapshot(), 300, 100.0, &h); - assert_eq!(m.icon, "error"); + assert_eq!(m.icon, "attention"); assert_eq!(m.sni_status, "NeedsAttention"); } #[test] @@ -426,73 +420,4 @@ mod tests { ); } } - #[test] - fn icon_ladder_covers_four_statuses_by_seven_health_states() { - let cases = [ - ( - connected_facts(), - ["recording", "idle", "paused", "stopped"], - ), - ( - SyncFacts { - in_progress: true, - link: Some(LinkFactState { - observer_registered: true, - ..Default::default() - }), - ..Default::default() - }, - ["syncing", "syncing", "paused", "stopped"], - ), - ( - SyncFacts { - last_error_class: Some(ErrorType::Transient), - ..Default::default() - }, - ["syncing", "syncing", "paused", "stopped"], - ), - ( - SyncFacts { - last_error_class: Some(ErrorType::Incompatible), - ..Default::default() - }, - ["error", "error", "error", "error"], - ), - ( - SyncFacts { - last_error_class: Some(ErrorType::Auth), - ..Default::default() - }, - ["error", "error", "error", "error"], - ), - ( - SyncFacts { - last_successful_contact: Some(0.0), - ..Default::default() - }, - ["syncing", "syncing", "paused", "stopped"], - ), - ( - SyncFacts::default(), - ["syncing", "syncing", "paused", "stopped"], - ), - ]; - let statuses = [ - TrayStatus::Recording, - TrayStatus::Idle, - TrayStatus::Paused, - TrayStatus::Stopped, - ]; - for (facts, expected) in cases { - let health = derive_health(&facts, 1_000.0, 600.0); - for (status, expected_icon) in statuses.into_iter().zip(expected) { - assert_eq!( - icon_name(status, &health), - expected_icon, - "status={status:?}, health={:?}", - health.state - ); - } - } - } }