c19702ae09be | atagen | 2026-05-27 15:49 UTC | 1 | //! `headroom` cli — connects to the daemon over its uds; `daemon` runs core in-process. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 2 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 3 | #![forbid(unsafe_code)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 4 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 5 | #[cfg(feature = "tui")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 6 | mod tui; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 7 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 8 | use std::path::PathBuf; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 9 | use std::process::ExitCode; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 10 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 11 | use clap::{Parser, Subcommand, ValueEnum}; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 12 | use headroom_client::{Client, ClientError, Route, Topic}; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 13 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 14 | // debug-only: arms core's audio-thread `assert_no_alloc` blocks; no-op in release. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 15 | #[cfg(debug_assertions)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 16 | #[global_allocator] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 17 | static ALLOCATOR: assert_no_alloc::AllocDisabler = assert_no_alloc::AllocDisabler; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 18 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 19 | /// headroom cli. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 20 | #[derive(Debug, Parser)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 21 | #[command(version, about, long_about = None)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 22 | struct Cli { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 23 | /// override the daemon control socket path. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 24 | #[arg(long, global = true)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 25 | socket: Option<PathBuf>, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 26 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 27 | #[command(subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 28 | cmd: Cmd, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 29 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 30 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 31 | #[derive(Debug, Subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 32 | enum Cmd { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 33 | /// run the daemon in the foreground. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 34 | Daemon { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 35 | /// extra `*.toml` profile dir (repeatable). precedes shipped/user |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 36 | /// profiles; first dir is watched for live edits. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 37 | #[arg(long = "profile-dir", value_name = "DIR")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 38 | profile_dir: Vec<PathBuf>, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 39 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 40 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 41 | /// show daemon status. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 42 | Status, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 43 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 44 | /// profile management. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 45 | #[command(subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 46 | Profile(ProfileCmd), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 47 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 48 | /// routing rules and per-stream decisions. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 49 | #[command(subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 50 | Route(RouteCmd), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 51 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 52 | /// per-app level control (layer a). |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 53 | #[command(subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 54 | PerApp(PerAppCmd), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 55 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 56 | /// get a setting value from the active profile. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 57 | Get { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 58 | /// dotted setting key. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 59 | key: String, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 60 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 61 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 62 | /// set a setting override (or clear with `--clear`). overrides persist |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 63 | /// across profile switches; `reset` clears all. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 64 | Set { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 65 | /// dotted setting key. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 66 | key: String, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 67 | /// new value, json-encoded. omit with `--clear`. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 68 | // allow_hyphen_values so negatives aren't parsed as flags. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 69 | #[arg(allow_hyphen_values = true)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 70 | value: Option<String>, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 71 | /// clear this key's override instead of setting one. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 72 | #[arg(long, conflicts_with = "value")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 73 | clear: bool, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 74 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 75 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 76 | /// toggle the global bypass kill switch. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 77 | Bypass { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 78 | /// `on` or `off`. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 79 | #[arg(value_enum)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 80 | state: BypassState, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 81 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 82 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 83 | /// clear all setting overrides. leaves route/per-app/bypass intact. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 84 | Reset, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 85 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 86 | /// reload profile files from disk. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 87 | Reload, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 88 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 89 | /// live monitor. defaults to a tui; `--json` is the line-delimited stream. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 90 | Monitor { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 91 | /// topics to subscribe to (comma-separated). `--json` only; the tui |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 92 | /// subscribes to all. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 93 | #[arg(value_delimiter = ',', default_value = "meters")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 94 | topics: Vec<MonitorTopic>, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 95 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 96 | /// emit one json event per line instead of the tui. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 97 | #[arg(long)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 98 | json: bool, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 99 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 100 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 101 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 102 | #[derive(Debug, Clone, Copy, ValueEnum)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 103 | enum MonitorTopic { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 104 | Meters, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 105 | Profile, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 106 | Routing, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 107 | Daemon, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 108 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 109 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 110 | impl From<MonitorTopic> for Topic { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 111 | fn from(t: MonitorTopic) -> Self { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 112 | match t { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 113 | MonitorTopic::Meters => Topic::Meters, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 114 | MonitorTopic::Profile => Topic::Profile, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 115 | MonitorTopic::Routing => Topic::Routing, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 116 | MonitorTopic::Daemon => Topic::Daemon, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 117 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 118 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 119 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 120 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 121 | #[derive(Debug, Subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 122 | enum ProfileCmd { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 123 | /// list known profiles. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 124 | List, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 125 | /// activate the named profile. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 126 | Use { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 127 | /// profile name. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 128 | name: String, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 129 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 130 | /// show a profile in full. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 131 | Show { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 132 | /// profile name (defaults to the active profile). |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 133 | name: Option<String>, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 134 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 135 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 136 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 137 | #[derive(Debug, Subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 138 | enum RouteCmd { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 139 | /// list routing rules and current per-stream decisions. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 140 | List, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 141 | /// add or replace a routing rule for an app. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 142 | Set { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 143 | /// app identifier (e.g. `application.process.binary`). |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 144 | app: String, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 145 | /// where to route. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 146 | #[arg(value_enum)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 147 | to: RouteArg, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 148 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 149 | /// remove an app's user routing rule. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 150 | Unset { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 151 | /// app identifier. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 152 | app: String, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 153 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 154 | /// reroute a specific live stream by node id. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 155 | Stream { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 156 | /// pipewire node id. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 157 | node_id: u32, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 158 | /// where to route. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 159 | #[arg(value_enum)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 160 | to: RouteArg, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 161 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 162 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 163 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 164 | #[derive(Debug, Subcommand)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 165 | enum PerAppCmd { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 166 | /// show per-app layer a state for managed streams. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 167 | Status { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 168 | /// emit the snapshot list as json instead of a table. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 169 | #[arg(long)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 170 | json: bool, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 171 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 172 | /// enable the layer a master switch (persisted). |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 173 | On, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 174 | /// disable the layer a master switch (persisted). |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 175 | Off, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 176 | /// enable or disable layer a for a specific app (persisted). |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 177 | Set { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 178 | /// app identifier (process_binary or application_name). |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 179 | app: String, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 180 | /// `on` or `off`. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 181 | #[arg(value_enum)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 182 | state: BypassState, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 183 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 184 | /// clear a managed stream's deference lock. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 185 | Reset { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 186 | /// pipewire node id of the managed stream. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 187 | node_id: u32, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 188 | }, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 189 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 190 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 191 | #[derive(Debug, Clone, Copy, ValueEnum)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 192 | enum BypassState { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 193 | On, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 194 | Off, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 195 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 196 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 197 | #[derive(Debug, Clone, Copy, ValueEnum)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 198 | enum RouteArg { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 199 | Processed, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 200 | Bypass, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 201 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 202 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 203 | impl From<RouteArg> for Route { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 204 | fn from(r: RouteArg) -> Self { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 205 | match r { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 206 | RouteArg::Processed => Route::Processed, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 207 | RouteArg::Bypass => Route::Bypass, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 208 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 209 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 210 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 211 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 212 | fn init_tracing() { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 213 | let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 214 | .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("headroom=info")); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 215 | tracing_subscriber::fmt() |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 216 | .with_env_filter(env_filter) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 217 | .with_target(false) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 218 | .compact() |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 219 | .init(); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 220 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 221 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 222 | fn run() -> Result<(), CliError> { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 223 | let cli = Cli::parse(); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 224 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 225 | // tui owns the terminal; keep `tracing` off it. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 226 | let tui_mode = matches!(&cli.cmd, Cmd::Monitor { json: false, .. }); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 227 | if !tui_mode { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 228 | init_tracing(); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 229 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 230 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 231 | match cli.cmd { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 232 | Cmd::Daemon { profile_dir } => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 233 | headroom_core::run_with_profile_dirs(profile_dir) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 234 | .map_err(|e| CliError::Daemon(e.to_string()))?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 235 | Ok(()) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 236 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 237 | Cmd::Monitor { json: false, .. } => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 238 | #[cfg(feature = "tui")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 239 | { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 240 | // connect on the main thread so initial rpcs run before raw mode. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 241 | let client = match cli.socket.as_deref() { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 242 | Some(p) => Client::connect_at(p)?, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 243 | None => Client::connect()?, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 244 | }; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 245 | tui::run(client).map_err(CliError::Tui) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 246 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 247 | // no `tui` feature: point at the json stream, not an opaque failure. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 248 | #[cfg(not(feature = "tui"))] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 249 | { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 250 | let _ = cli.socket.as_deref(); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 251 | Err(CliError::Other( |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 252 | "this build was compiled without the TUI (`tui` feature disabled); \ |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 253 | rerun `headroom monitor --json` for the line-delimited event stream" |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 254 | .into(), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 255 | )) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 256 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 257 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 258 | cmd => with_client(cli.socket.as_deref(), |c| dispatch(c, cmd)), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 259 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 260 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 261 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 262 | fn with_client<F>(socket: Option<&std::path::Path>, f: F) -> Result<(), CliError> |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 263 | where |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 264 | F: FnOnce(&mut Client) -> Result<(), CliError>, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 265 | { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 266 | let mut client = match socket { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 267 | Some(p) => Client::connect_at(p)?, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 268 | None => Client::connect()?, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 269 | }; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 270 | f(&mut client) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 271 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 272 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 273 | fn dispatch(client: &mut Client, cmd: Cmd) -> Result<(), CliError> { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 274 | match cmd { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 275 | Cmd::Daemon { .. } => unreachable!("handled in `run`"), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 276 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 277 | Cmd::Status => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 278 | let status = client.status()?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 279 | println!("{}", serde_json::to_string_pretty(&status)?); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 280 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 281 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 282 | Cmd::Profile(ProfileCmd::List) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 283 | let profiles = client.profile_list()?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 284 | for p in profiles { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 285 | let marker = if p.active { '*' } else { ' ' }; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 286 | println!("{marker} {:<16} {}", p.name, p.description); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 287 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 288 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 289 | Cmd::Profile(ProfileCmd::Use { name }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 290 | let active = client.profile_use(&name)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 291 | println!("active profile: {active}"); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 292 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 293 | Cmd::Profile(ProfileCmd::Show { name }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 294 | let body = client.profile_show(name.as_deref())?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 295 | println!("{}", serde_json::to_string_pretty(&body)?); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 296 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 297 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 298 | Cmd::Route(RouteCmd::List) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 299 | let list = client.route_list()?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 300 | println!("{}", serde_json::to_string_pretty(&list)?); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 301 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 302 | Cmd::Route(RouteCmd::Set { app, to }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 303 | client.route_set(&app, to.into())?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 304 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 305 | Cmd::Route(RouteCmd::Unset { app }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 306 | client.route_unset(&app)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 307 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 308 | Cmd::Route(RouteCmd::Stream { node_id, to }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 309 | client.route_stream(node_id, to.into())?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 310 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 311 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 312 | Cmd::PerApp(PerAppCmd::Status { json }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 313 | let list = client.layer_a_list()?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 314 | if json { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 315 | println!("{}", serde_json::to_string_pretty(&list)?); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 316 | } else if list.is_empty() { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 317 | println!("no streams under Layer A management"); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 318 | } else { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 319 | println!( |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 320 | "{:<8} {:<24} {:>10} {:>9} {:>9}", |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 321 | "node", "app", "reduction", "ceiling", "deferred" |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 322 | ); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 323 | for s in &list { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 324 | let app = if s.app.len() > 24 { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 325 | format!("{}…", &s.app[..23]) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 326 | } else { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 327 | s.app.clone() |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 328 | }; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 329 | let ceiling = s |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 330 | .user_ceiling_lin |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 331 | .map(|c| format!("{c:.2}")) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 332 | .unwrap_or_else(|| "—".to_string()); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 333 | println!( |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 334 | "{:<8} {:<24} {:>8.1}dB {:>9} {:>9}", |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 335 | s.node_id, app, s.reduction_db, ceiling, s.deferred |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 336 | ); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 337 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 338 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 339 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 340 | Cmd::PerApp(PerAppCmd::On) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 341 | client.per_app_master(true)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 342 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 343 | Cmd::PerApp(PerAppCmd::Off) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 344 | client.per_app_master(false)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 345 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 346 | Cmd::PerApp(PerAppCmd::Set { app, state }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 347 | client.per_app_set(&app, matches!(state, BypassState::On))?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 348 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 349 | Cmd::PerApp(PerAppCmd::Reset { node_id }) => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 350 | client.layer_a_reset(node_id)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 351 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 352 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 353 | Cmd::Get { key } => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 354 | let v = client.setting_get(&key)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 355 | println!("{}", serde_json::to_string(&v)?); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 356 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 357 | Cmd::Set { key, value, clear } => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 358 | if clear { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 359 | let existed = client.setting_clear(&key)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 360 | if existed { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 361 | println!("cleared override for '{key}'"); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 362 | } else { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 363 | println!("no override set for '{key}'"); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 364 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 365 | } else { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 366 | let value = value.ok_or_else(|| { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 367 | CliError::Other("provide a value, or use --clear to remove the override".into()) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 368 | })?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 369 | let parsed: serde_json::Value = serde_json::from_str(&value) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 370 | .map_err(|e| CliError::Other(format!("value is not valid JSON: {e}")))?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 371 | client.setting_set(&key, parsed)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 372 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 373 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 374 | Cmd::Bypass { state } => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 375 | client.bypass_set(matches!(state, BypassState::On))?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 376 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 377 | Cmd::Reset => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 378 | let cleared = client.setting_reset()?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 379 | println!("cleared {cleared} setting override(s)"); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 380 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 381 | Cmd::Reload => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 382 | let reloaded = client.profile_reload()?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 383 | println!("reloaded: {reloaded:?}"); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 384 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 385 | Cmd::Monitor { topics, json } => { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 386 | if json { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 387 | let pw_topics: Vec<Topic> = |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 388 | topics.iter().copied().map(Topic::from).collect(); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 389 | client.subscribe(&pw_topics)?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 390 | loop { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 391 | let ev = client.next_event()?; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 392 | println!( |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 393 | "{} {}/{} {}", |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 394 | chrono_like_now(), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 395 | ev.topic, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 396 | ev.event, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 397 | serde_json::to_string(&ev.data)?, |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 398 | ); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 399 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 400 | } else { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 401 | unreachable!("TUI monitor is dispatched before `with_client`") |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 402 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 403 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 404 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 405 | Ok(()) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 406 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 407 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 408 | #[derive(Debug, thiserror::Error)] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 409 | enum CliError { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 410 | #[error("client: {0}")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 411 | Client(#[from] ClientError), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 412 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 413 | #[error("daemon: {0}")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 414 | Daemon(String), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 415 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 416 | #[error("json: {0}")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 417 | Json(#[from] serde_json::Error), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 418 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 419 | #[cfg(feature = "tui")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 420 | #[error("tui: {0}")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 421 | Tui(tui::TuiError), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 422 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 423 | #[error("{0}")] |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 424 | Other(String), |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 425 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 426 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 427 | /// timestamp label for monitor output; `SystemTime` to avoid chrono. |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 428 | fn chrono_like_now() -> String { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 429 | use std::time::{SystemTime, UNIX_EPOCH}; |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 430 | let t = SystemTime::now() |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 431 | .duration_since(UNIX_EPOCH) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 432 | .unwrap_or_default(); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 433 | format!("{}.{:03}", t.as_secs(), t.subsec_millis()) |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 434 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 435 | |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 436 | fn main() -> ExitCode { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 437 | if let Err(e) = run() { |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 438 | eprintln!("headroom: {e}"); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 439 | return ExitCode::from(1); |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 440 | } |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 441 | ExitCode::SUCCESS |
c19702ae09be | atagen | 2026-05-27 15:49 UTC | 442 | } |