feat(tray): system-tray status icon for the host (Windows + Linux)
New crates/punktfunk-tray — a small per-user companion showing the host service state at a glance (running / stopped / starting / degraded / failed + the live session in the tooltip) with one-click actions: open web console, approve a pending pairing request, start/stop/restart, open logs. No more digging through logs to learn whether the service came back after a reboot or an update. Status is service-manager-FIRST (SCM / systemd user unit — a port squatter can never fake Running), then the new loopback-only unauthenticated GET /api/v1/local/summary (counts/booleans only; the mgmt token and cert.pem are SYSTEM/Admins-DACL'd on Windows, so a non-elevated tray cannot bearer-auth). Windows: windows_subsystem binary (a console exe in the Run key would flash a terminal at sign-in), Shell_NotifyIcon + hidden window, per-session single instance, TaskbarCreated re-add, --quit for the uninstaller; service actions elevate per click via ShellExecuteW "runas" onto the new `punktfunk-host service restart` (stop → wait Stopped → start). Linux: ksni/StatusNotifierItem over zbus, systemctl --user actions (no polkit), /etc/xdg/autostart entry whose --autostart self-gates to actual host users. Icons: scripts/gen-tray-icons.py (pure stdlib) renders the brand lens + status dot into committed .ico/hicolor assets; deb/rpm/arch ship binary+autostart+icons. Live-validated: Linux on the headless KDE session (SNI registration, state transitions, menu-driven start, dbusmenu layout); Windows on the RTX box (session-1 launch with no NIM_ADD failure, single instance, --quit, restart round-trip, summary loopback-200/LAN-401). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
//! punktfunk-tray — a small per-user system-tray companion for the punktfunk host service.
|
||||
//!
|
||||
//! Shows at a glance whether the host is running / stopped / degraded / failed (no more digging
|
||||
//! through logs after a reboot or an update), and offers the common one-click actions: open the
|
||||
//! web console, start/stop/restart the service (UAC-elevated per action on Windows,
|
||||
//! `systemctl --user` on Linux), review a pending pairing request, exit.
|
||||
//!
|
||||
//! Status comes from two sources, service manager FIRST (a fake listener on the mgmt port can
|
||||
//! never make a stopped service look running): the SCM / systemd user unit for the process state,
|
||||
//! then the host's loopback-only unauthenticated `GET /api/v1/local/summary` for the streaming
|
||||
//! details. Windows-subsystem binary — a console exe in the HKLM Run key would flash a terminal
|
||||
//! window at every sign-in.
|
||||
#![cfg_attr(windows, windows_subsystem = "windows")]
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
mod status;
|
||||
#[cfg(windows)]
|
||||
mod win;
|
||||
|
||||
/// CLI configuration (hand-rolled parse, house style). The mgmt address/port default to the
|
||||
/// host's defaults; they are flags because the tray cannot read `host.env` on Windows (it is
|
||||
/// DACL-locked to SYSTEM/Administrators), so an operator who moved `--mgmt-bind` adjusts the
|
||||
/// autostart command line instead.
|
||||
pub struct Args {
|
||||
/// Ask an already-running tray instance to exit (Windows; used by the uninstaller).
|
||||
pub quit: bool,
|
||||
/// Launched from the desktop autostart entry: exit silently when this box doesn't run a host
|
||||
/// (Linux; the package installs the autostart file for every desktop user).
|
||||
pub autostart: bool,
|
||||
/// Management API address to poll (loopback only; the summary route rejects anything else).
|
||||
pub mgmt_addr: String,
|
||||
pub mgmt_port: u16,
|
||||
/// Web console port for the "Open web console" action.
|
||||
pub web_port: u16,
|
||||
}
|
||||
|
||||
impl Default for Args {
|
||||
fn default() -> Self {
|
||||
Args {
|
||||
quit: false,
|
||||
autostart: false,
|
||||
mgmt_addr: "127.0.0.1".into(),
|
||||
mgmt_port: 47990,
|
||||
web_port: 47992,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args() -> anyhow::Result<Args> {
|
||||
let mut args = Args::default();
|
||||
let mut it = std::env::args().skip(1);
|
||||
while let Some(a) = it.next() {
|
||||
let mut value = |flag: &str| {
|
||||
it.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("{flag} needs a value"))
|
||||
};
|
||||
match a.as_str() {
|
||||
"--quit" => args.quit = true,
|
||||
"--autostart" => args.autostart = true,
|
||||
"--mgmt-addr" => args.mgmt_addr = value("--mgmt-addr")?,
|
||||
"--mgmt-port" => args.mgmt_port = value("--mgmt-port")?.parse()?,
|
||||
"--web-port" => args.web_port = value("--web-port")?.parse()?,
|
||||
"--version" | "-V" => {
|
||||
println!("punktfunk-tray {}", env!("CARGO_PKG_VERSION"));
|
||||
std::process::exit(0);
|
||||
}
|
||||
other => anyhow::bail!(
|
||||
"unknown argument '{other}'\n\nUSAGE:\n punktfunk-tray [--autostart] [--quit] \
|
||||
[--mgmt-addr <IP>] [--mgmt-port <N>] [--web-port <N>]"
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = parse_args()?;
|
||||
run(args)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn run(args: Args) -> anyhow::Result<()> {
|
||||
win::run(args)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn run(args: Args) -> anyhow::Result<()> {
|
||||
linux::run(args)
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
fn run(_args: Args) -> anyhow::Result<()> {
|
||||
// Workspace-stub build (macOS CI etc.) — the tray ships on Windows and Linux only.
|
||||
anyhow::bail!("punktfunk-tray supports Windows and Linux hosts only")
|
||||
}
|
||||
Reference in New Issue
Block a user