fix(tray): drop the conflicting-host warning — the tray is the wrong place for it
ci / web (push) Successful in 59s
ci / docs-site (push) Successful in 1m6s
ci / bench (push) Successful in 7m0s
deb / build-publish (push) Failing after 7m35s
decky / build-publish (push) Successful in 22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
deb / build-publish-host (push) Failing after 8m6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
android / android (push) Successful in 13m38s
ci / rust-arm64 (push) Successful in 12m28s
docker / deploy-docs (push) Successful in 24s
arch / build-publish (push) Successful in 14m10s
windows-host / package (push) Successful in 10m56s
deb / build-publish-client-arm64 (push) Successful in 8m36s
docker / build-push-arm64cross (push) Successful in 4m37s
apple / swift (push) Successful in 5m28s
ci / rust (push) Successful in 22m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 13m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m0s
apple / screenshots (push) Successful in 23m51s

The tray led its tooltip with "⚠ conflicting host: Sunshine" and (on Linux)
went NeedsAttention for a Sunshine that was merely *installed* — never
started, never listening, harmless. An always-on warning over a dormant
package is a false alarm on every poll, and the tray is the wrong surface
for a one-shot install-time observation anyway.

Removes the `conflicts` field from the tray's `Summary`, the tooltip prefix,
`has_conflicts()`, and the ksni attention branch that fed on it. The host
still detects and reports conflicts where that belongs — the startup
`punktfunk::detect` warning, the `detect-conflicts` subcommand, and the
`/api/v1/local/summary` field, which the tray must (and does, per the new
test) keep tolerating on the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 20:53:44 +02:00
co-authored by Claude Opus 5
parent ffc7aec91a
commit 3e7828522d
2 changed files with 31 additions and 51 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ impl ksni::Tray for HostTray {
fn status(&self) -> ksni::Status { fn status(&self) -> ksni::Status {
match &self.status { match &self.status {
TrayStatus::Error(_) => ksni::Status::NeedsAttention, TrayStatus::Error(_) => ksni::Status::NeedsAttention,
s if s.pairing_attention() || s.has_conflicts() => ksni::Status::NeedsAttention, s if s.pairing_attention() => ksni::Status::NeedsAttention,
_ => ksni::Status::Active, _ => ksni::Status::Active,
} }
} }
+30 -50
View File
@@ -37,11 +37,6 @@ pub struct Summary {
/// host that doesn't send it deserializes as 0. /// host that doesn't send it deserializes as 0.
#[serde(default)] #[serde(default)]
pub kept_displays: u32, pub kept_displays: u32,
/// Other Moonlight-compatible hosts (Sunshine/Apollo/…) the host detected on this machine at
/// startup — side-by-side use is unsupported. `#[serde(default)]` so an older host omitting it
/// deserializes as empty.
#[serde(default)]
pub conflicts: Vec<String>,
} }
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
@@ -74,45 +69,26 @@ impl TrayStatus {
TrayStatus::Starting => "punktfunk host — starting…".into(), TrayStatus::Starting => "punktfunk host — starting…".into(),
TrayStatus::Degraded => "punktfunk host — running (status unavailable)".into(), TrayStatus::Degraded => "punktfunk host — running (status unavailable)".into(),
TrayStatus::Error(e) => format!("punktfunk host — failed ({e})"), TrayStatus::Error(e) => format!("punktfunk host — failed ({e})"),
TrayStatus::Running(s) => { TrayStatus::Running(s) => match (&s.session, self.is_streaming()) {
let base = match (&s.session, self.is_streaming()) { (Some(sess), true) => format!(
(Some(sess), true) => format!( "punktfunk host {} — streaming {}×{}@{}",
"punktfunk host {} — streaming {}×{}@{}", s.version, sess.width, sess.height, sess.fps
s.version, sess.width, sess.height, sess.fps ),
), (_, true) => format!("punktfunk host {} — streaming", s.version),
(_, true) => format!("punktfunk host {} — streaming", s.version), // Idle, but surface a kept (lingering/pinned) display: it — and, under an
// Idle, but surface a kept (lingering/pinned) display: it — and, under an // exclusive topology, your physical monitors — is being held. Release it from
// exclusive topology, your physical monitors — is being held. Release it from // the console.
// the console. _ if s.kept_displays > 0 => format!(
_ if s.kept_displays > 0 => format!( "punktfunk host {} — idle · {} display{} kept",
"punktfunk host {} — idle · {} display{} kept", s.version,
s.version, s.kept_displays,
s.kept_displays, if s.kept_displays == 1 { "" } else { "s" }
if s.kept_displays == 1 { "" } else { "s" } ),
), _ => format!("punktfunk host {} — idle", s.version),
_ => format!("punktfunk host {} — idle", s.version), },
};
// A conflicting Moonlight host (Sunshine/Apollo/…) is the loudest thing to say —
// side-by-side use is unsupported, so lead the tooltip with it.
if s.conflicts.is_empty() {
base
} else {
format!("⚠ conflicting host: {}{base}", s.conflicts.join(", "))
}
}
} }
} }
/// The host detected another Moonlight-compatible host (Sunshine/Apollo/…) on this machine —
/// unsupported side-by-side. Drives the Linux (ksni) backend's `NeedsAttention` state; the
/// Windows backend surfaces the same conflict through the tooltip `headline()` instead (it has
/// no distinct attention icon), so this accessor is unused there — allow it per-platform rather
/// than gate the shared API out.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn has_conflicts(&self) -> bool {
matches!(self, TrayStatus::Running(s) if !s.conflicts.is_empty())
}
/// A client is streaming: the host's flag, OR a live session in the summary. /// A client is streaming: the host's flag, OR a live session in the summary.
/// ///
/// The session is checked too because a host from before the `get_local_summary` fix raised /// The session is checked too because a host from before the `get_local_summary` fix raised
@@ -433,7 +409,6 @@ mod tests {
pin_pending: false, pin_pending: false,
pending_approvals: 0, pending_approvals: 0,
kept_displays: 0, kept_displays: 0,
conflicts: Vec::new(),
} }
} }
@@ -476,15 +451,20 @@ mod tests {
} }
} }
/// Conflicting-host detection is deliberately NOT a tray concern: "installed" is not
/// "running", and an always-on ⚠ over a merely-present Sunshine cried wolf. The host still
/// detects and reports it (startup log, `detect-conflicts`, the mgmt summary) — the tray just
/// ignores that field, which it must keep tolerating on the wire.
#[test] #[test]
fn conflicts_drive_attention_and_lead_the_tooltip() { fn a_summary_carrying_conflicts_still_deserializes_and_is_ignored() {
let mut s = summary(false); let json = r#"{"version":"0.5.1","video_streaming":false,"audio_streaming":false,
assert!(!TrayStatus::Running(s.clone()).has_conflicts()); "session":null,"paired_clients":1,"native_paired_clients":2,"pin_pending":false,
s.conflicts = vec!["Sunshine (running)".into(), "Apollo".into()]; "pending_approvals":0,"kept_displays":0,"conflicts":["Sunshine (installed)"]}"#;
let st = TrayStatus::Running(s); let s: Summary = serde_json::from_str(json).expect("unknown fields are ignored");
assert!(st.has_conflicts()); assert_eq!(
let head = st.headline(); TrayStatus::Running(s).headline(),
assert!(head.starts_with("⚠ conflicting host: Sunshine (running), Apollo")); "punktfunk host 0.5.1 — idle"
);
} }
#[test] #[test]