ebada804b6
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>
98 lines
3.7 KiB
Rust
98 lines
3.7 KiB
Rust
//! 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")
|
|
}
|