[chore](trx-client): remove appkit frontend support
Remove macOS AppKit frontend (trx-frontend-appkit) and related code: - Delete appkit crate directory - Remove appkit dependency and feature from Cargo.toml - Remove appkit imports, main thread handling, and config from main.rs - Remove AppKit config struct from config.rs - Remove appkit section from example config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
Generated
-185
@@ -373,15 +373,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block2"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.2"
|
version = "8.0.2"
|
||||||
@@ -723,16 +714,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -1426,158 +1407,6 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "oboe"
|
name = "oboe"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -2390,7 +2219,6 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"trx-core",
|
"trx-core",
|
||||||
"trx-frontend",
|
"trx-frontend",
|
||||||
"trx-frontend-appkit",
|
|
||||||
"trx-frontend-http",
|
"trx-frontend-http",
|
||||||
"trx-frontend-http-json",
|
"trx-frontend-http-json",
|
||||||
"trx-frontend-rigctl",
|
"trx-frontend-rigctl",
|
||||||
@@ -2414,19 +2242,6 @@ dependencies = [
|
|||||||
"trx-core",
|
"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]]
|
[[package]]
|
||||||
name = "trx-frontend-http"
|
name = "trx-frontend-http"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ members = [
|
|||||||
"src/trx-client/trx-frontend",
|
"src/trx-client/trx-frontend",
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http",
|
"src/trx-client/trx-frontend/trx-frontend-http",
|
||||||
"src/trx-client/trx-frontend/trx-frontend-http-json",
|
"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-client/trx-frontend/trx-frontend-rigctl",
|
||||||
"src/trx-core",
|
"src/trx-core",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ trx-frontend = { path = "trx-frontend" }
|
|||||||
trx-frontend-http = { path = "trx-frontend/trx-frontend-http" }
|
trx-frontend-http = { path = "trx-frontend/trx-frontend-http" }
|
||||||
trx-frontend-http-json = { path = "trx-frontend/trx-frontend-http-json" }
|
trx-frontend-http-json = { path = "trx-frontend/trx-frontend-http-json" }
|
||||||
trx-frontend-rigctl = { path = "trx-frontend/trx-frontend-rigctl" }
|
trx-frontend-rigctl = { path = "trx-frontend/trx-frontend-rigctl" }
|
||||||
trx-frontend-appkit = { path = "trx-frontend/trx-frontend-appkit", optional = true }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
appkit-frontend = ["trx-frontend-appkit/appkit"]
|
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ pub struct FrontendsConfig {
|
|||||||
pub rigctl: RigctlFrontendConfig,
|
pub rigctl: RigctlFrontendConfig,
|
||||||
/// JSON TCP frontend settings
|
/// JSON TCP frontend settings
|
||||||
pub http_json: HttpJsonFrontendConfig,
|
pub http_json: HttpJsonFrontendConfig,
|
||||||
/// AppKit (macOS) frontend settings
|
|
||||||
pub appkit: AppKitFrontendConfig,
|
|
||||||
/// Audio streaming settings
|
/// Audio streaming settings
|
||||||
pub audio: AudioClientConfig,
|
pub audio: AudioClientConfig,
|
||||||
}
|
}
|
||||||
@@ -188,14 +186,6 @@ pub struct HttpJsonAuthConfig {
|
|||||||
pub tokens: Vec<String>,
|
pub tokens: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
impl ClientConfig {
|
||||||
/// Load configuration from a specific file path.
|
/// Load configuration from a specific file path.
|
||||||
pub fn load_from_file(path: &Path) -> Result<Self, ConfigError> {
|
pub fn load_from_file(path: &Path) -> Result<Self, ConfigError> {
|
||||||
@@ -265,7 +255,6 @@ impl ClientConfig {
|
|||||||
port: 4532,
|
port: 4532,
|
||||||
},
|
},
|
||||||
http_json: HttpJsonFrontendConfig::default(),
|
http_json: HttpJsonFrontendConfig::default(),
|
||||||
appkit: AppKitFrontendConfig { enabled: false },
|
|
||||||
audio: AudioClientConfig::default(),
|
audio: AudioClientConfig::default(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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_http_json::{register_frontend as register_http_json_frontend, set_auth_tokens};
|
||||||
use trx_frontend_rigctl::register_frontend as register_rigctl_frontend;
|
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 config::ClientConfig;
|
||||||
use remote_client::{parse_remote_url, RemoteClientConfig};
|
use remote_client::{parse_remote_url, RemoteClientConfig};
|
||||||
|
|
||||||
@@ -99,27 +94,8 @@ fn normalize_name(name: &str) -> String {
|
|||||||
fn main() -> DynResult<()> {
|
fn main() -> DynResult<()> {
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
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 {
|
rt.block_on(async {
|
||||||
signal::ctrl_c().await?;
|
signal::ctrl_c().await?;
|
||||||
info!("Ctrl+C received, shutting down");
|
info!("Ctrl+C received, shutting down");
|
||||||
@@ -128,14 +104,7 @@ fn main() -> DynResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Holds the state needed after async initialization completes.
|
/// Holds the state needed after async initialization completes.
|
||||||
struct AppState {
|
struct AppState;
|
||||||
#[allow(dead_code)]
|
|
||||||
has_appkit: bool,
|
|
||||||
#[cfg(feature = "appkit-frontend")]
|
|
||||||
state_rx: watch::Receiver<RigState>,
|
|
||||||
#[cfg(feature = "appkit-frontend")]
|
|
||||||
rig_tx: mpsc::Sender<RigRequest>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn async_init() -> DynResult<AppState> {
|
async fn async_init() -> DynResult<AppState> {
|
||||||
tracing_subscriber::fmt().with_target(false).init();
|
tracing_subscriber::fmt().with_target(false).init();
|
||||||
@@ -143,8 +112,6 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
register_http_frontend();
|
register_http_frontend();
|
||||||
register_http_json_frontend();
|
register_http_json_frontend();
|
||||||
register_rigctl_frontend();
|
register_rigctl_frontend();
|
||||||
#[cfg(feature = "appkit-frontend")]
|
|
||||||
register_appkit_frontend();
|
|
||||||
let _plugin_libs = plugins::load_plugins();
|
let _plugin_libs = plugins::load_plugins();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -200,9 +167,6 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
if cfg.frontends.http_json.enabled {
|
if cfg.frontends.http_json.enabled {
|
||||||
fes.push("httpjson".to_string());
|
fes.push("httpjson".to_string());
|
||||||
}
|
}
|
||||||
if cfg.frontends.appkit.enabled {
|
|
||||||
fes.push("appkit".to_string());
|
|
||||||
}
|
|
||||||
if fes.is_empty() {
|
if fes.is_empty() {
|
||||||
fes.push("http".to_string());
|
fes.push("http".to_string());
|
||||||
}
|
}
|
||||||
@@ -232,8 +196,6 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
.clone()
|
.clone()
|
||||||
.or_else(|| cfg.general.callsign.clone());
|
.or_else(|| cfg.general.callsign.clone());
|
||||||
|
|
||||||
let has_appkit = frontends.iter().any(|f| f == "appkit");
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Starting trx-client (remote: {}, frontends: {})",
|
"Starting trx-client (remote: {}, frontends: {})",
|
||||||
remote_addr,
|
remote_addr,
|
||||||
@@ -327,11 +289,8 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
info!("Audio disabled in config, decode will not be available");
|
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 {
|
for frontend in &frontends {
|
||||||
if frontend == "appkit" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let frontend_state_rx = state_rx.clone();
|
let frontend_state_rx = state_rx.clone();
|
||||||
let addr = match frontend.as_str() {
|
let addr = match frontend.as_str() {
|
||||||
"http" => SocketAddr::from((http_listen, http_port)),
|
"http" => SocketAddr::from((http_listen, http_port)),
|
||||||
@@ -350,11 +309,5 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AppState {
|
Ok(AppState)
|
||||||
has_appkit,
|
|
||||||
#[cfg(feature = "appkit-frontend")]
|
|
||||||
state_rx,
|
|
||||||
#[cfg(feature = "appkit-frontend")]
|
|
||||||
rig_tx: tx,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
|
||||||
#
|
|
||||||
# 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"
|
|
||||||
] }
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
|
||||||
//
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
|
||||||
//
|
|
||||||
// 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<RigRequest>`.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
|
||||||
//
|
|
||||||
// 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<RigState>,
|
|
||||||
_rig_tx: mpsc::Sender<RigRequest>,
|
|
||||||
_callsign: Option<String>,
|
|
||||||
listen_addr: SocketAddr,
|
|
||||||
) -> JoinHandle<()> {
|
|
||||||
let (state_update_tx, _state_update_rx) = std::sync::mpsc::channel::<RigState>();
|
|
||||||
|
|
||||||
// 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<RigState>,
|
|
||||||
rig_tx: mpsc::Sender<RigRequest>,
|
|
||||||
) {
|
|
||||||
// Channel for state updates: async watcher -> main thread.
|
|
||||||
let (state_update_tx, state_update_rx) = std::sync::mpsc::channel::<RigState>();
|
|
||||||
|
|
||||||
// Channel for button actions: UI buttons -> main thread loop.
|
|
||||||
let (action_tx, action_rx) = std::sync::mpsc::channel::<ButtonAction>();
|
|
||||||
|
|
||||||
// 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<RigRequest>,
|
|
||||||
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<RigRequest>, 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<RigState>,
|
|
||||||
state_update_tx: std::sync::mpsc::Sender<RigState>,
|
|
||||||
) {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
|
||||||
//
|
|
||||||
// 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<NSTextField>,
|
|
||||||
pub mode_label: Retained<NSTextField>,
|
|
||||||
pub band_label: Retained<NSTextField>,
|
|
||||||
pub ptt_label: Retained<NSTextField>,
|
|
||||||
pub lock_label: Retained<NSTextField>,
|
|
||||||
pub power_label: Retained<NSTextField>,
|
|
||||||
pub rx_sig_label: Retained<NSTextField>,
|
|
||||||
pub tx_power_label: Retained<NSTextField>,
|
|
||||||
pub tx_limit_label: Retained<NSTextField>,
|
|
||||||
pub tx_swr_label: Retained<NSTextField>,
|
|
||||||
pub tx_alc_label: Retained<NSTextField>,
|
|
||||||
pub vfo_label: Retained<NSTextField>,
|
|
||||||
// Input fields for reading user input from button actions
|
|
||||||
pub freq_input: Retained<NSTextField>,
|
|
||||||
pub mode_input: Retained<NSTextField>,
|
|
||||||
pub tx_limit_input: Retained<NSTextField>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<RigRequest>) {
|
|
||||||
let val = self.freq_input.stringValue();
|
|
||||||
let text = val.to_string();
|
|
||||||
if let Ok(hz) = text.trim().parse::<u64>() {
|
|
||||||
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<RigRequest>) {
|
|
||||||
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<RigRequest>) {
|
|
||||||
let val = self.tx_limit_input.stringValue();
|
|
||||||
if let Ok(limit) = val.to_string().trim().parse::<u8>() {
|
|
||||||
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<NSTextField> {
|
|
||||||
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<NSTextField> {
|
|
||||||
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<NSTextField>) -> Retained<NSView> {
|
|
||||||
Retained::into_super(Retained::into_super(field))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert an NSButton into an NSView (NSButton -> NSControl -> NSView).
|
|
||||||
fn button_to_view(btn: Retained<NSButton>) -> Retained<NSView> {
|
|
||||||
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<ButtonAction>,
|
|
||||||
) -> (Retained<NSWindow>, 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::<NSWindow>(),
|
|
||||||
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<Retained<NSView>> = 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<RigRequest>, 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<ButtonAction>,
|
|
||||||
) -> Retained<NSButton> {
|
|
||||||
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<ButtonAction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static BUTTON_ACTIONS: std::sync::OnceLock<std::sync::Mutex<Vec<ActionEntry>>> =
|
|
||||||
std::sync::OnceLock::new();
|
|
||||||
|
|
||||||
fn action_table() -> &'static std::sync::Mutex<Vec<ActionEntry>> {
|
|
||||||
BUTTON_ACTIONS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_button_action(
|
|
||||||
action: ButtonAction,
|
|
||||||
sender: std::sync::mpsc::Sender<ButtonAction>,
|
|
||||||
) -> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,7 +47,3 @@ listen = "127.0.0.1"
|
|||||||
port = 0
|
port = 0
|
||||||
# List of accepted bearer tokens (empty = no auth)
|
# List of accepted bearer tokens (empty = no auth)
|
||||||
# auth.tokens = ["example-token"]
|
# auth.tokens = ["example-token"]
|
||||||
|
|
||||||
[frontends.appkit]
|
|
||||||
# Enable AppKit GUI frontend (macOS only, requires appkit-frontend feature)
|
|
||||||
enabled = false
|
|
||||||
|
|||||||
Reference in New Issue
Block a user