From c6bdaa6184020bdda7f9a729f4aafa74240434fb Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Thu, 20 Aug 2026 13:31:03 -0600 Subject: [PATCH] Make the tray mark answer whether the journal is receiving. Map the twelve health states onto the six ruled marks (healthy, attention, paused, offline, error, connecting) and embed all six vendored status SVGs as tray pixmaps (previously four). Capture-idle no longer demotes the mark; capture-stopped no longer aliases to error. Unrecognized names floor to offline rather than healthy. Replace the owner-facing health copy. --- crates/solstone-linux/build.rs | 6 +- crates/solstone-linux/src/cli.rs | 2 +- crates/solstone-linux/src/sync.rs | 21 +- crates/solstone-linux/src/sync_health.rs | 215 ++++++++++---------- crates/solstone-linux/src/tray.rs | 238 ++++++++++++++++++++++- crates/solstone-linux/src/tray_model.rs | 117 ++--------- 6 files changed, 376 insertions(+), 223 deletions(-) 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 - ); - } - } - } } -- 2.51.2