diff --git a/Cargo.lock b/Cargo.lock index c5ec900..f5ffc4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,15 +373,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - [[package]] name = "brotli" version = "8.0.2" @@ -723,16 +714,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.10.0", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1426,158 +1407,6 @@ dependencies = [ "syn", ] -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-foundation", -] - [[package]] name = "oboe" version = "0.6.1" @@ -2390,7 +2219,6 @@ dependencies = [ "tracing-subscriber", "trx-core", "trx-frontend", - "trx-frontend-appkit", "trx-frontend-http", "trx-frontend-http-json", "trx-frontend-rigctl", @@ -2414,19 +2242,6 @@ dependencies = [ "trx-core", ] -[[package]] -name = "trx-frontend-appkit" -version = "0.1.0" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-foundation", - "tokio", - "tracing", - "trx-core", - "trx-frontend", -] - [[package]] name = "trx-frontend-http" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e0b0a81..54d396c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ members = [ "src/trx-client/trx-frontend", "src/trx-client/trx-frontend/trx-frontend-http", "src/trx-client/trx-frontend/trx-frontend-http-json", - "src/trx-client/trx-frontend/trx-frontend-appkit", "src/trx-client/trx-frontend/trx-frontend-rigctl", "src/trx-core", ] diff --git a/src/trx-client/Cargo.toml b/src/trx-client/Cargo.toml index 3672dbe..ac06354 100644 --- a/src/trx-client/Cargo.toml +++ b/src/trx-client/Cargo.toml @@ -23,8 +23,6 @@ trx-frontend = { path = "trx-frontend" } trx-frontend-http = { path = "trx-frontend/trx-frontend-http" } trx-frontend-http-json = { path = "trx-frontend/trx-frontend-http-json" } trx-frontend-rigctl = { path = "trx-frontend/trx-frontend-rigctl" } -trx-frontend-appkit = { path = "trx-frontend/trx-frontend-appkit", optional = true } [features] default = [] -appkit-frontend = ["trx-frontend-appkit/appkit"] diff --git a/src/trx-client/src/config.rs b/src/trx-client/src/config.rs index 8383521..d3071a2 100644 --- a/src/trx-client/src/config.rs +++ b/src/trx-client/src/config.rs @@ -86,8 +86,6 @@ pub struct FrontendsConfig { pub rigctl: RigctlFrontendConfig, /// JSON TCP frontend settings pub http_json: HttpJsonFrontendConfig, - /// AppKit (macOS) frontend settings - pub appkit: AppKitFrontendConfig, /// Audio streaming settings pub audio: AudioClientConfig, } @@ -188,14 +186,6 @@ pub struct HttpJsonAuthConfig { pub tokens: Vec, } -/// AppKit (macOS) frontend configuration. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct AppKitFrontendConfig { - /// Whether AppKit frontend is enabled - pub enabled: bool, -} - impl ClientConfig { /// Load configuration from a specific file path. pub fn load_from_file(path: &Path) -> Result { @@ -265,7 +255,6 @@ impl ClientConfig { port: 4532, }, http_json: HttpJsonFrontendConfig::default(), - appkit: AppKitFrontendConfig { enabled: false }, audio: AudioClientConfig::default(), }, }; diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index 187fb6f..3d2743e 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -30,11 +30,6 @@ use trx_frontend_http::{register_frontend as register_http_frontend, set_audio_c use trx_frontend_http_json::{register_frontend as register_http_json_frontend, set_auth_tokens}; use trx_frontend_rigctl::register_frontend as register_rigctl_frontend; -#[cfg(feature = "appkit-frontend")] -use trx_frontend_appkit::register_frontend as register_appkit_frontend; -#[cfg(feature = "appkit-frontend")] -use trx_frontend_appkit::run_appkit_main_thread; - use config::ClientConfig; use remote_client::{parse_remote_url, RemoteClientConfig}; @@ -99,27 +94,8 @@ fn normalize_name(name: &str) -> String { fn main() -> DynResult<()> { let rt = tokio::runtime::Runtime::new()?; - #[allow(unused_variables)] - let app_state = rt.block_on(async_init())?; + let _app_state = rt.block_on(async_init())?; - #[cfg(feature = "appkit-frontend")] - if app_state.has_appkit { - // Keep a runtime context active on the main thread so that - // tokio::spawn inside run_appkit_main_thread works. - let _guard = rt.enter(); - - // AppKit needs the process main thread. Spawn Ctrl+C handler on the - // runtime, then hand main thread to AppKit (blocks forever). - rt.spawn(async { - signal::ctrl_c().await.ok(); - info!("Ctrl+C received, shutting down"); - std::process::exit(0); - }); - run_appkit_main_thread(app_state.state_rx, app_state.rig_tx); - unreachable!(); - } - - // No AppKit — block on Ctrl+C as before. rt.block_on(async { signal::ctrl_c().await?; info!("Ctrl+C received, shutting down"); @@ -128,14 +104,7 @@ fn main() -> DynResult<()> { } /// Holds the state needed after async initialization completes. -struct AppState { - #[allow(dead_code)] - has_appkit: bool, - #[cfg(feature = "appkit-frontend")] - state_rx: watch::Receiver, - #[cfg(feature = "appkit-frontend")] - rig_tx: mpsc::Sender, -} +struct AppState; async fn async_init() -> DynResult { tracing_subscriber::fmt().with_target(false).init(); @@ -143,8 +112,6 @@ async fn async_init() -> DynResult { register_http_frontend(); register_http_json_frontend(); register_rigctl_frontend(); - #[cfg(feature = "appkit-frontend")] - register_appkit_frontend(); let _plugin_libs = plugins::load_plugins(); let cli = Cli::parse(); @@ -200,9 +167,6 @@ async fn async_init() -> DynResult { if cfg.frontends.http_json.enabled { fes.push("httpjson".to_string()); } - if cfg.frontends.appkit.enabled { - fes.push("appkit".to_string()); - } if fes.is_empty() { fes.push("http".to_string()); } @@ -232,8 +196,6 @@ async fn async_init() -> DynResult { .clone() .or_else(|| cfg.general.callsign.clone()); - let has_appkit = frontends.iter().any(|f| f == "appkit"); - info!( "Starting trx-client (remote: {}, frontends: {})", remote_addr, @@ -327,11 +289,8 @@ async fn async_init() -> DynResult { info!("Audio disabled in config, decode will not be available"); } - // Spawn frontends (skip appkit — it will be driven from main thread) + // Spawn frontends for frontend in &frontends { - if frontend == "appkit" { - continue; - } let frontend_state_rx = state_rx.clone(); let addr = match frontend.as_str() { "http" => SocketAddr::from((http_listen, http_port)), @@ -350,11 +309,5 @@ async fn async_init() -> DynResult { )?; } - Ok(AppState { - has_appkit, - #[cfg(feature = "appkit-frontend")] - state_rx, - #[cfg(feature = "appkit-frontend")] - rig_tx: tx, - }) + Ok(AppState) } diff --git a/src/trx-client/trx-frontend/trx-frontend-appkit/Cargo.toml b/src/trx-client/trx-frontend/trx-frontend-appkit/Cargo.toml deleted file mode 100644 index c19a195..0000000 --- a/src/trx-client/trx-frontend/trx-frontend-appkit/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Stanislaw Grams -# -# SPDX-License-Identifier: BSD-2-Clause - -[package] -name = "trx-frontend-appkit" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -default = [] -appkit = ["dep:objc2", "dep:objc2-foundation", "dep:objc2-app-kit"] - -[dependencies] -trx-core = { path = "../../../trx-core" } -trx-frontend = { path = ".." } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } - -[target.'cfg(target_os = "macos")'.dependencies] -objc2 = { version = "0.6", optional = true } -objc2-foundation = { version = "0.3", optional = true, features = ["NSString", "NSThread", "NSRunLoop"] } -objc2-app-kit = { version = "0.3", optional = true, features = [ - "NSApplication", "NSWindow", "NSView", "NSTextField", - "NSButton", "NSStackView", "NSColor", "NSResponder", - "NSControl", "NSRunningApplication", "NSGraphics" -] } diff --git a/src/trx-client/trx-frontend/trx-frontend-appkit/src/helpers.rs b/src/trx-client/trx-frontend/trx-frontend-appkit/src/helpers.rs deleted file mode 100644 index 60c2eaf..0000000 --- a/src/trx-client/trx-frontend/trx-frontend-appkit/src/helpers.rs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Stanislaw Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -use trx_core::rig::state::{RigMode, RigState}; - -pub fn format_freq(hz: u64) -> String { - if hz >= 1_000_000_000 { - format!("{:.3} GHz", hz as f64 / 1_000_000_000.0) - } else if hz >= 10_000_000 { - format!("{:.3} MHz", hz as f64 / 1_000_000.0) - } else if hz >= 1_000 { - format!("{:.1} kHz", hz as f64 / 1_000.0) - } else { - format!("{hz} Hz") - } -} - -pub fn mode_label(mode: &RigMode) -> String { - match mode { - RigMode::LSB => "LSB".to_string(), - RigMode::USB => "USB".to_string(), - RigMode::CW => "CW".to_string(), - RigMode::CWR => "CWR".to_string(), - RigMode::AM => "AM".to_string(), - RigMode::WFM => "WFM".to_string(), - RigMode::FM => "FM".to_string(), - RigMode::DIG => "DIG".to_string(), - RigMode::PKT => "PKT".to_string(), - RigMode::Other(val) => val.clone(), - } -} - -pub fn parse_mode(value: &str) -> RigMode { - match value.trim().to_uppercase().as_str() { - "LSB" => RigMode::LSB, - "USB" => RigMode::USB, - "CW" => RigMode::CW, - "CWR" => RigMode::CWR, - "AM" => RigMode::AM, - "FM" => RigMode::FM, - "WFM" => RigMode::WFM, - "DIG" | "DIGI" => RigMode::DIG, - "PKT" | "PACKET" => RigMode::PKT, - other => RigMode::Other(other.to_string()), - } -} - -pub fn vfo_label(state: &RigState) -> String { - let Some(vfo) = state.status.vfo.as_ref() else { - return "--".to_string(); - }; - - let mut lines = Vec::new(); - for (idx, entry) in vfo.entries.iter().enumerate() { - let marker = if vfo.active == Some(idx) { "*" } else { " " }; - let freq = format_freq(entry.freq.hz); - let mode = entry - .mode - .as_ref() - .map(mode_label) - .unwrap_or_else(|| "--".to_string()); - lines.push(format!("{marker} {}: {} {}", entry.name, freq, mode)); - } - lines.join("\n") -} diff --git a/src/trx-client/trx-frontend/trx-frontend-appkit/src/lib.rs b/src/trx-client/trx-frontend/trx-frontend-appkit/src/lib.rs deleted file mode 100644 index 3da456f..0000000 --- a/src/trx-client/trx-frontend/trx-frontend-appkit/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Stanislaw Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -#[cfg(all(target_os = "macos", feature = "appkit"))] -pub mod helpers; -#[cfg(all(target_os = "macos", feature = "appkit"))] -pub mod model; -#[cfg(all(target_os = "macos", feature = "appkit"))] -pub mod server; -#[cfg(all(target_os = "macos", feature = "appkit"))] -pub mod ui; - -#[cfg(all(target_os = "macos", feature = "appkit"))] -pub use server::run_appkit_main_thread; - -#[cfg(all(target_os = "macos", feature = "appkit"))] -pub fn register_frontend() { - use trx_frontend::FrontendSpawner; - trx_frontend::register_frontend("appkit", server::AppKitFrontend::spawn_frontend); -} - -#[cfg(not(all(target_os = "macos", feature = "appkit")))] -pub fn register_frontend() { - // No-op on non-macOS platforms or when appkit feature is disabled. -} diff --git a/src/trx-client/trx-frontend/trx-frontend-appkit/src/model.rs b/src/trx-client/trx-frontend/trx-frontend-appkit/src/model.rs deleted file mode 100644 index b3066c4..0000000 --- a/src/trx-client/trx-frontend/trx-frontend-appkit/src/model.rs +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Stanislaw Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -//! Platform-agnostic rig state model. -//! -//! This struct holds the display-ready state derived from `RigState`. -//! The UI layer reads from this model; command sending is handled -//! separately via an `mpsc::Sender`. - -use trx_core::rig::state::RigState; - -use crate::helpers::{format_freq, mode_label, vfo_label}; - -/// Display-ready rig state. Updated from `RigState` on each change. -#[derive(Debug, Clone)] -pub struct RigStateModel { - pub freq_hz: u64, - pub freq_text: String, - pub mode: String, - pub band: String, - pub tx_enabled: bool, - pub locked: bool, - pub powered: bool, - pub rx_sig: i32, - pub tx_power: i32, - pub tx_limit: i32, - pub tx_swr: f64, - pub tx_alc: i32, - pub vfo: String, -} - -impl Default for RigStateModel { - fn default() -> Self { - Self { - freq_hz: 0, - freq_text: "-- Hz".to_string(), - mode: "--".to_string(), - band: "--".to_string(), - tx_enabled: false, - locked: false, - powered: false, - rx_sig: 0, - tx_power: 0, - tx_limit: 0, - tx_swr: 0.0, - tx_alc: 0, - vfo: "--".to_string(), - } - } -} - -impl RigStateModel { - /// Update all fields from a `RigState` snapshot. Returns `true` if anything changed. - pub fn update(&mut self, state: &RigState) -> bool { - let mut changed = false; - - let freq_hz = state.status.freq.hz; - if self.freq_hz != freq_hz { - self.freq_hz = freq_hz; - self.freq_text = format_freq(freq_hz); - changed = true; - } - - let mode = mode_label(&state.status.mode); - if self.mode != mode { - self.mode = mode; - changed = true; - } - - let band = state.band_name().unwrap_or_else(|| "--".to_string()); - if self.band != band { - self.band = band; - changed = true; - } - - if self.tx_enabled != state.status.tx_en { - self.tx_enabled = state.status.tx_en; - changed = true; - } - - let locked = state.status.lock.unwrap_or(false); - if self.locked != locked { - self.locked = locked; - changed = true; - } - - let powered = state.control.enabled.unwrap_or(false); - if self.powered != powered { - self.powered = powered; - changed = true; - } - - let rx_sig = state - .status - .rx - .as_ref() - .and_then(|rx| rx.sig) - .unwrap_or(0); - if self.rx_sig != rx_sig { - self.rx_sig = rx_sig; - changed = true; - } - - let tx_power = state - .status - .tx - .as_ref() - .and_then(|tx| tx.power) - .map(i32::from) - .unwrap_or(0); - if self.tx_power != tx_power { - self.tx_power = tx_power; - changed = true; - } - - let tx_limit = state - .status - .tx - .as_ref() - .and_then(|tx| tx.limit) - .map(i32::from) - .unwrap_or(0); - if self.tx_limit != tx_limit { - self.tx_limit = tx_limit; - changed = true; - } - - let tx_swr = state - .status - .tx - .as_ref() - .and_then(|tx| tx.swr) - .unwrap_or(0.0) as f64; - if (self.tx_swr - tx_swr).abs() > f64::EPSILON { - self.tx_swr = tx_swr; - changed = true; - } - - let tx_alc = state - .status - .tx - .as_ref() - .and_then(|tx| tx.alc) - .map(i32::from) - .unwrap_or(0); - if self.tx_alc != tx_alc { - self.tx_alc = tx_alc; - changed = true; - } - - let vfo = vfo_label(state); - if self.vfo != vfo { - self.vfo = vfo; - changed = true; - } - - changed - } -} diff --git a/src/trx-client/trx-frontend/trx-frontend-appkit/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-appkit/src/server.rs deleted file mode 100644 index 6bcf998..0000000 --- a/src/trx-client/trx-frontend/trx-frontend-appkit/src/server.rs +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Stanislaw Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -//! AppKit frontend spawner. -//! -//! Spawns a dedicated thread for the NSApplication run loop and an async -//! task that watches for rig state changes and pushes them to the UI -//! thread via a std::sync::mpsc channel. - -use std::net::SocketAddr; - -use objc2::MainThreadMarker; -use objc2_app_kit::NSApplication; -use tokio::sync::{mpsc, watch}; -use tokio::task::JoinHandle; -use tracing::{info, warn}; - -use trx_core::rig::command::RigCommand; -use trx_core::{RigRequest, RigState}; -use trx_frontend::FrontendSpawner; - -use crate::model::RigStateModel; -use crate::ui::{self, ButtonAction, UiElements}; - -/// AppKit frontend implementation. -pub struct AppKitFrontend; - -impl FrontendSpawner for AppKitFrontend { - fn spawn_frontend( - state_rx: watch::Receiver, - _rig_tx: mpsc::Sender, - _callsign: Option, - listen_addr: SocketAddr, - ) -> JoinHandle<()> { - let (state_update_tx, _state_update_rx) = std::sync::mpsc::channel::(); - - // Spawn async state watcher that forwards state changes. - // The actual AppKit event loop is driven by `run_appkit_main_thread` - // called from main() on the process main thread. - tokio::spawn(async move { - info!("AppKit frontend starting (addr hint: {})", listen_addr); - run_state_watcher(state_rx, state_update_tx).await; - }) - } -} - -/// Run the AppKit event loop on the calling thread (must be the process main -/// thread, i.e. thread 0). This function **blocks forever**. -/// -/// It creates the NSApplication, builds the UI window, and enters a polling -/// loop that drains AppKit events, applies rig state updates, and dispatches -/// button actions. -pub fn run_appkit_main_thread( - state_rx: watch::Receiver, - rig_tx: mpsc::Sender, -) { - // Channel for state updates: async watcher -> main thread. - let (state_update_tx, state_update_rx) = std::sync::mpsc::channel::(); - - // Channel for button actions: UI buttons -> main thread loop. - let (action_tx, action_rx) = std::sync::mpsc::channel::(); - - // Spawn async state watcher onto the tokio runtime (running on a - // background thread). - tokio::spawn(async move { - run_state_watcher(state_rx, state_update_tx).await; - }); - - let mtm = MainThreadMarker::new() - .expect("run_appkit_main_thread must be called from the process main thread"); - - let app = NSApplication::sharedApplication(mtm); - - let (window, ui_elements) = ui::build_window(mtm, action_tx); - - // Keep window alive for the process lifetime. - std::mem::forget(window); - - let mut model = RigStateModel::default(); - - info!("AppKit frontend: entering main run loop"); - - // Run a polling loop instead of NSApplication::run() so we can - // process state updates and button actions between event cycles. - loop { - // Process pending AppKit events. - drain_appkit_events(&app); - - // Process state updates from the async watcher. - while let Ok(state) = state_update_rx.try_recv() { - if model.update(&state) { - ui_elements.refresh(&model); - } - } - - // Process button actions. - while let Ok(action) = action_rx.try_recv() { - handle_action(action, &ui_elements, &rig_tx, &model); - } - - // Sleep briefly to avoid busy-waiting. - std::thread::sleep(std::time::Duration::from_millis(16)); - } -} - -fn drain_appkit_events(app: &NSApplication) { - use objc2_app_kit::NSEventMask; - use objc2_foundation::NSDate; - - loop { - let event = unsafe { - app.nextEventMatchingMask_untilDate_inMode_dequeue( - NSEventMask::Any, - Some(&NSDate::distantPast()), - objc2_foundation::NSDefaultRunLoopMode, - true, - ) - }; - match event { - Some(event) => { - app.sendEvent(&event); - } - None => break, - } - } -} - -fn handle_action( - action: ButtonAction, - ui: &UiElements, - rig_tx: &mpsc::Sender, - model: &RigStateModel, -) { - match action { - ButtonAction::TogglePtt => { - send_command(rig_tx, RigCommand::SetPtt(!model.tx_enabled)); - } - ButtonAction::TogglePower => { - if model.powered { - send_command(rig_tx, RigCommand::PowerOff); - } else { - send_command(rig_tx, RigCommand::PowerOn); - } - } - ButtonAction::ToggleVfo => { - send_command(rig_tx, RigCommand::ToggleVfo); - } - ButtonAction::ToggleLock => { - if model.locked { - send_command(rig_tx, RigCommand::Unlock); - } else { - send_command(rig_tx, RigCommand::Lock); - } - } - ButtonAction::SetFreq => { - ui.handle_set_freq(rig_tx); - } - ButtonAction::SetMode => { - ui.handle_set_mode(rig_tx); - } - ButtonAction::SetTxLimit => { - ui.handle_set_tx_limit(rig_tx); - } - } -} - -fn send_command(tx: &mpsc::Sender, cmd: RigCommand) { - let (resp_tx, _resp_rx) = tokio::sync::oneshot::channel(); - if tx - .blocking_send(RigRequest { - cmd, - respond_to: resp_tx, - }) - .is_err() - { - warn!("AppKit frontend: rig command send failed"); - } -} - -async fn run_state_watcher( - mut state_rx: watch::Receiver, - state_update_tx: std::sync::mpsc::Sender, -) { - // Send initial state. - let _ = state_update_tx.send(state_rx.borrow().clone()); - - while state_rx.changed().await.is_ok() { - let state = state_rx.borrow().clone(); - if state_update_tx.send(state).is_err() { - warn!("AppKit frontend: state update channel closed"); - break; - } - } -} diff --git a/src/trx-client/trx-frontend/trx-frontend-appkit/src/ui.rs b/src/trx-client/trx-frontend/trx-frontend-appkit/src/ui.rs deleted file mode 100644 index fa2da13..0000000 --- a/src/trx-client/trx-frontend/trx-frontend-appkit/src/ui.rs +++ /dev/null @@ -1,351 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Stanislaw Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -//! AppKit window and controls. -//! -//! Creates an NSWindow with labels for rig status and buttons for rig -//! control. All AppKit calls must run on the main thread. - -use objc2::rc::Retained; -use objc2::{msg_send, MainThreadMarker}; -use objc2_app_kit::{ - NSBackingStoreType, NSButton, NSColor, NSStackView, NSTextField, NSView, NSWindow, - NSWindowStyleMask, -}; -use objc2_foundation::{NSArray, NSEdgeInsets, NSPoint, NSRect, NSSize, NSString}; - -use tokio::sync::{mpsc, oneshot}; -use tracing::warn; - -use trx_core::radio::freq::Freq; -use trx_core::rig::command::RigCommand; -use trx_core::rig::request::RigRequest; - -use crate::helpers::parse_mode; -use crate::model::RigStateModel; - -/// All UI elements that need updating when rig state changes. -/// These must only be accessed from the AppKit main thread. -pub struct UiElements { - pub freq_label: Retained, - pub mode_label: Retained, - pub band_label: Retained, - pub ptt_label: Retained, - pub lock_label: Retained, - pub power_label: Retained, - pub rx_sig_label: Retained, - pub tx_power_label: Retained, - pub tx_limit_label: Retained, - pub tx_swr_label: Retained, - pub tx_alc_label: Retained, - pub vfo_label: Retained, - // Input fields for reading user input from button actions - pub freq_input: Retained, - pub mode_input: Retained, - pub tx_limit_input: Retained, -} - -impl UiElements { - /// Refresh all labels from the model. - pub fn refresh(&self, model: &RigStateModel) { - set_label_text(&self.freq_label, &model.freq_text); - set_label_text(&self.mode_label, &format!("Mode: {}", model.mode)); - set_label_text(&self.band_label, &format!("Band: {}", model.band)); - set_label_text( - &self.ptt_label, - if model.tx_enabled { "PTT: TX" } else { "PTT: RX" }, - ); - set_label_text( - &self.lock_label, - if model.locked { - "Lock: ON" - } else { - "Lock: OFF" - }, - ); - set_label_text( - &self.power_label, - if model.powered { - "Power: ON" - } else { - "Power: OFF" - }, - ); - set_label_text(&self.rx_sig_label, &format!("RX Sig: {}", model.rx_sig)); - set_label_text( - &self.tx_power_label, - &format!("TX Power: {}", model.tx_power), - ); - set_label_text( - &self.tx_limit_label, - &format!("TX Limit: {}", model.tx_limit), - ); - set_label_text( - &self.tx_swr_label, - &format!("SWR: {:.1}", model.tx_swr), - ); - set_label_text(&self.tx_alc_label, &format!("ALC: {}", model.tx_alc)); - set_label_text(&self.vfo_label, &format!("VFO: {}", model.vfo)); - } - - /// Read the frequency input field value and send a SetFreq command. - pub fn handle_set_freq(&self, rig_tx: &mpsc::Sender) { - let val = self.freq_input.stringValue(); - let text = val.to_string(); - if let Ok(hz) = text.trim().parse::() { - if hz > 0 { - send_command(rig_tx, RigCommand::SetFreq(Freq { hz })); - } - } - } - - /// Read the mode input field value and send a SetMode command. - pub fn handle_set_mode(&self, rig_tx: &mpsc::Sender) { - let val = self.mode_input.stringValue(); - let mode = parse_mode(&val.to_string()); - send_command(rig_tx, RigCommand::SetMode(mode)); - } - - /// Read the TX limit input field value and send a SetTxLimit command. - pub fn handle_set_tx_limit(&self, rig_tx: &mpsc::Sender) { - let val = self.tx_limit_input.stringValue(); - if let Ok(limit) = val.to_string().trim().parse::() { - send_command(rig_tx, RigCommand::SetTxLimit(limit)); - } - } -} - -fn set_label_text(label: &NSTextField, text: &str) { - let ns = NSString::from_str(text); - label.setStringValue(&ns); -} - -fn make_label(mtm: MainThreadMarker, text: &str) -> Retained { - let ns = NSString::from_str(text); - let label = NSTextField::labelWithString(&ns, mtm); - label.setEditable(false); - label.setBordered(false); - label.setDrawsBackground(false); - label.setTextColor(Some(&NSColor::labelColor())); - label -} - -fn make_editable_field(mtm: MainThreadMarker, placeholder: &str) -> Retained { - let ns = NSString::from_str(placeholder); - let field = NSTextField::textFieldWithString(&ns, mtm); - field.setEditable(true); - field.setBordered(true); - field -} - -/// Convert an NSTextField into an NSView (NSTextField -> NSControl -> NSView). -fn text_field_to_view(field: Retained) -> Retained { - Retained::into_super(Retained::into_super(field)) -} - -/// Convert an NSButton into an NSView (NSButton -> NSControl -> NSView). -fn button_to_view(btn: Retained) -> Retained { - Retained::into_super(Retained::into_super(btn)) -} - -/// Actions that buttons can trigger. Sent via a channel to be handled -/// on the AppKit thread where UI elements live. -#[derive(Debug, Clone, Copy)] -pub enum ButtonAction { - TogglePtt, - TogglePower, - ToggleVfo, - ToggleLock, - SetFreq, - SetMode, - SetTxLimit, -} - -/// Build the main window with status labels and control buttons. -/// -/// `action_tx` is a channel sender for button actions — each button stores -/// its action tag and the server's run-loop timer reads these to dispatch. -/// -/// Returns the window (which must be kept alive) and the UI elements -/// struct for later updates. -pub fn build_window( - mtm: MainThreadMarker, - action_tx: std::sync::mpsc::Sender, -) -> (Retained, UiElements) { - let style = NSWindowStyleMask::Titled - | NSWindowStyleMask::Closable - | NSWindowStyleMask::Miniaturizable - | NSWindowStyleMask::Resizable; - - let frame = NSRect::new(NSPoint::new(200.0, 200.0), NSSize::new(400.0, 520.0)); - let window = unsafe { - NSWindow::initWithContentRect_styleMask_backing_defer( - mtm.alloc::(), - frame, - style, - NSBackingStoreType::Buffered, - false, - ) - }; - - let title = NSString::from_str("trx-rs"); - window.setTitle(&title); - - // Status labels - let freq_label = make_label(mtm, "-- Hz"); - let mode_label = make_label(mtm, "Mode: --"); - let band_label = make_label(mtm, "Band: --"); - let ptt_label = make_label(mtm, "PTT: RX"); - let lock_label = make_label(mtm, "Lock: OFF"); - let power_label = make_label(mtm, "Power: OFF"); - let rx_sig_label = make_label(mtm, "RX Sig: 0"); - let tx_power_label = make_label(mtm, "TX Power: 0"); - let tx_limit_label = make_label(mtm, "TX Limit: 0"); - let tx_swr_label = make_label(mtm, "SWR: 0.0"); - let tx_alc_label = make_label(mtm, "ALC: 0"); - let vfo_label = make_label(mtm, "VFO: --"); - - // Control buttons — each stores an action tag, actions are dispatched - // via the global action table. - let ptt_btn = make_button(mtm, "Toggle PTT", ButtonAction::TogglePtt, &action_tx); - let power_btn = make_button(mtm, "Toggle Power", ButtonAction::TogglePower, &action_tx); - let vfo_btn = make_button(mtm, "Toggle VFO", ButtonAction::ToggleVfo, &action_tx); - let lock_btn = make_button(mtm, "Toggle Lock", ButtonAction::ToggleLock, &action_tx); - - // Input fields - let freq_input = make_editable_field(mtm, "Freq (Hz)"); - let set_freq_btn = make_button(mtm, "Set Freq", ButtonAction::SetFreq, &action_tx); - - let mode_input = make_editable_field(mtm, "Mode (USB, LSB, ...)"); - let set_mode_btn = make_button(mtm, "Set Mode", ButtonAction::SetMode, &action_tx); - - let tx_limit_input = make_editable_field(mtm, "TX Limit (0-255)"); - let set_tx_limit_btn = make_button(mtm, "Set TX Limit", ButtonAction::SetTxLimit, &action_tx); - - // Build vertical stack view - let views: Vec> = vec![ - text_field_to_view(freq_label.clone()), - text_field_to_view(mode_label.clone()), - text_field_to_view(band_label.clone()), - text_field_to_view(ptt_label.clone()), - text_field_to_view(lock_label.clone()), - text_field_to_view(power_label.clone()), - text_field_to_view(rx_sig_label.clone()), - text_field_to_view(tx_power_label.clone()), - text_field_to_view(tx_limit_label.clone()), - text_field_to_view(tx_swr_label.clone()), - text_field_to_view(tx_alc_label.clone()), - text_field_to_view(vfo_label.clone()), - button_to_view(ptt_btn), - button_to_view(power_btn), - button_to_view(vfo_btn), - button_to_view(lock_btn), - text_field_to_view(freq_input.clone()), - button_to_view(set_freq_btn), - text_field_to_view(mode_input.clone()), - button_to_view(set_mode_btn), - text_field_to_view(tx_limit_input.clone()), - button_to_view(set_tx_limit_btn), - ]; - - let ns_views = NSArray::from_retained_slice(&views); - let stack = NSStackView::stackViewWithViews(&ns_views, mtm); - stack.setOrientation(objc2_app_kit::NSUserInterfaceLayoutOrientation::Vertical); - stack.setSpacing(8.0); - stack.setEdgeInsets(NSEdgeInsets { - top: 16.0, - left: 16.0, - bottom: 16.0, - right: 16.0, - }); - - window.setContentView(Some(&stack)); - window.makeKeyAndOrderFront(None); - - let ui = UiElements { - freq_label, - mode_label, - band_label, - ptt_label, - lock_label, - power_label, - rx_sig_label, - tx_power_label, - tx_limit_label, - tx_swr_label, - tx_alc_label, - vfo_label, - freq_input, - mode_input, - tx_limit_input, - }; - - (window, ui) -} - -fn send_command(tx: &mpsc::Sender, cmd: RigCommand) { - let (resp_tx, _resp_rx) = oneshot::channel(); - if tx - .blocking_send(RigRequest { - cmd, - respond_to: resp_tx, - }) - .is_err() - { - warn!("AppKit frontend: rig command send failed"); - } -} - -fn make_button( - mtm: MainThreadMarker, - title: &str, - action: ButtonAction, - action_tx: &std::sync::mpsc::Sender, -) -> Retained { - let title_ns = NSString::from_str(title); - let btn = unsafe { - NSButton::buttonWithTitle_target_action(&title_ns, None, None, mtm) - }; - - // Store the action in the global table, indexed by the button's tag. - let idx = register_button_action(action, action_tx.clone()); - unsafe { - let _: () = msg_send![&btn, setTag: idx as isize]; - }; - - btn -} - -// Global action table for buttons. -struct ActionEntry { - action: ButtonAction, - sender: std::sync::mpsc::Sender, -} - -static BUTTON_ACTIONS: std::sync::OnceLock>> = - std::sync::OnceLock::new(); - -fn action_table() -> &'static std::sync::Mutex> { - BUTTON_ACTIONS.get_or_init(|| std::sync::Mutex::new(Vec::new())) -} - -fn register_button_action( - action: ButtonAction, - sender: std::sync::mpsc::Sender, -) -> usize { - let mut table = action_table().lock().unwrap(); - let idx = table.len(); - table.push(ActionEntry { action, sender }); - idx -} - -/// Call the action for a button given its tag index. -/// This sends the button's action through the channel for processing -/// on the main thread where UI elements are accessible. -pub fn invoke_button_action(tag: isize) { - let table = action_table().lock().unwrap(); - if let Some(entry) = table.get(tag as usize) { - let _ = entry.sender.send(entry.action); - } -} diff --git a/trx-client.toml.example b/trx-client.toml.example index 95ebaad..330adbd 100644 --- a/trx-client.toml.example +++ b/trx-client.toml.example @@ -47,7 +47,3 @@ listen = "127.0.0.1" port = 0 # List of accepted bearer tokens (empty = no auth) # auth.tokens = ["example-token"] - -[frontends.appkit] -# Enable AppKit GUI frontend (macOS only, requires appkit-frontend feature) -enabled = false