diff --git a/Cargo.lock b/Cargo.lock index 79a1436..dc2287e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,9 +311,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "block-buffer" @@ -333,15 +330,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2", -] - [[package]] name = "blocking" version = "1.6.1" @@ -372,12 +360,24 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.9.0" @@ -598,46 +598,12 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.10.0", - "core-graphics-types", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.10.0", - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.16" @@ -656,15 +622,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -771,27 +728,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -814,12 +750,6 @@ dependencies = [ "syn 2.0.91", ] -[[package]] -name = "dpi" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" - [[package]] name = "either" version = "1.13.0" @@ -958,28 +888,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.91", + "foreign-types-shared", ] [[package]] @@ -988,12 +897,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1689,6 +1592,18 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "2.7.0" @@ -1772,14 +1687,16 @@ dependencies = [ ] [[package]] -name = "keyboard-types" -version = "0.7.0" +name = "ksni" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +checksum = "3cc9a5e60d55371fd681051b05e9b58e1d818f5085f6364afe872c9347311f91" dependencies = [ - "bitflags 2.6.0", + "futures-util", + "paste", "serde", - "unicode-segmentation", + "tokio", + "zbus 5.2.0", ] [[package]] @@ -1788,30 +1705,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading 0.7.4", - "once_cell", -] - [[package]] name = "libc" version = "0.2.169" @@ -1928,25 +1821,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "muda" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "png", - "thiserror 1.0.69", - "windows-sys 0.59.0", -] - [[package]] name = "native-tls" version = "0.2.12" @@ -2069,105 +1943,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.6.0", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.6.0", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-encode" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.6.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.6.0", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.6.0", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - [[package]] name = "object" version = "0.36.7" @@ -2203,7 +1978,7 @@ checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", - "foreign-types 0.3.2", + "foreign-types", "libc", "once_cell", "openssl-macros", @@ -2239,12 +2014,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "ordered-stream" version = "0.2.0" @@ -2808,7 +2577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", - "core-foundation 0.9.4", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -2992,14 +2761,14 @@ dependencies = [ "futures", "gtk", "hex", + "image", "ipnet", + "ksni", "once_cell", - "png", "snxcore", "tokio", "tracing", "tracing-subscriber", - "tray-icon", "zbus 5.2.0", ] @@ -3147,7 +2916,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", - "core-foundation 0.9.4", + "core-foundation", "system-configuration-sys", ] @@ -3435,26 +3204,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tray-icon" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" -dependencies = [ - "core-graphics", - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "png", - "thiserror 1.0.69", - "windows-sys 0.59.0", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -3511,12 +3260,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/snx-rs-gui/Cargo.toml b/snx-rs-gui/Cargo.toml index a2d1917..9ccbb98 100644 --- a/snx-rs-gui/Cargo.toml +++ b/snx-rs-gui/Cargo.toml @@ -12,9 +12,8 @@ publish.workspace = true [dependencies] snxcore = { path = "../snxcore" } -tray-icon = { version = "0.19", default-features = false } gtk = "0.18" -png = "0.17" +image = { version = "0.25", default-features = false, features = ["png"] } async-channel = "2" tracing = "0.1" tracing-subscriber = "0.3" @@ -26,3 +25,4 @@ clap = { version = "4", features = ["derive"] } hex = "0.4" zbus = { version = "5", default-features = false, features = ["tokio"] } futures = "0.3" +ksni = "0.3" diff --git a/snx-rs-gui/src/assets.rs b/snx-rs-gui/src/assets.rs index 1212eda..1e41e6d 100644 --- a/snx-rs-gui/src/assets.rs +++ b/snx-rs-gui/src/assets.rs @@ -1,16 +1,14 @@ -use std::io; - use once_cell::sync::Lazy; fn png_to_argb(data: &[u8]) -> anyhow::Result> { - let decoder = png::Decoder::new(io::Cursor::new(data)); - let mut reader = decoder.read_info()?; - let mut buf = vec![0; reader.output_buffer_size()]; + let png = image::load_from_memory_with_format(data, image::ImageFormat::Png)?; + let mut img = png.to_rgba8(); - let info = reader.next_frame(&mut buf)?; - let bytes = buf[..info.buffer_size()].to_vec(); + for image::Rgba(pixel) in img.pixels_mut() { + *pixel = u32::from_be_bytes(*pixel).rotate_right(8).to_be_bytes(); + } - Ok(bytes) + Ok(img.into_raw()) } pub struct IconTheme { diff --git a/snx-rs-gui/src/main.rs b/snx-rs-gui/src/main.rs index 9cc74ab..567947e 100644 --- a/snx-rs-gui/src/main.rs +++ b/snx-rs-gui/src/main.rs @@ -7,12 +7,10 @@ use gtk::{ Application, License, }; use tracing::level_filters::LevelFilter; -use tray_icon::menu::MenuEvent; use snxcore::{controller::ServiceCommand, model::params::TunnelParams, platform::SingleInstance}; -use crate::theme::init_theme_monitoring; -use crate::tray::TrayCommand; +use crate::{theme::init_theme_monitoring, tray::TrayCommand}; mod assets; mod dbus; @@ -21,9 +19,11 @@ mod prompt; mod settings; mod theme; mod tray; + const PING_DURATION: Duration = Duration::from_secs(1); -fn main() -> anyhow::Result<()> { +#[tokio::main] +async fn main() -> anyhow::Result<()> { let params = params::CmdlineParams::parse(); let tunnel_params = Arc::new(TunnelParams::load(params.config_file()).unwrap_or_default()); @@ -44,72 +44,74 @@ fn main() -> anyhow::Result<()> { tracing::subscriber::set_global_default(subscriber).unwrap(); - let _ = init_theme_monitoring(); + let _ = init_theme_monitoring().await; let app = Application::builder().application_id("com.github.snx-rs").build(); - app.connect_activate(move |_| { - let params = params.clone(); + app.connect_activate(move |_| gtk::main()); + + let params = params.clone(); - let mut my_tray = tray::AppTray::new(¶ms).unwrap(); - let sender = my_tray.sender(); + let (event_sender, mut event_receiver) = tokio::sync::mpsc::channel(256); - let tx_copy = sender.clone(); - std::thread::spawn(move || loop { - let _ = tx_copy.send_blocking(TrayCommand::Service(ServiceCommand::Status)); - std::thread::sleep(PING_DURATION); - }); + let mut my_tray = tray::AppTray::new(¶ms, event_sender); + let sender = my_tray.sender(); + let tx_copy = sender.clone(); - if tunnel_params.ike_persist { - let _ = sender.send_blocking(TrayCommand::Service(ServiceCommand::Connect)); + tokio::spawn(async move { my_tray.run().await }); + + tokio::spawn(async move { + loop { + let _ = tx_copy.send(TrayCommand::Service(ServiceCommand::Status)).await; + tokio::time::sleep(PING_DURATION).await; } + }); - std::thread::spawn(move || { - while let Ok(v) = MenuEvent::receiver().recv() { - match v.id.0.as_str() { - "connect" => { - let _ = sender.send_blocking(TrayCommand::Service(ServiceCommand::Connect)); - } - "disconnect" => { - let _ = sender.send_blocking(TrayCommand::Service(ServiceCommand::Disconnect)); - } - "settings" => { - let params = TunnelParams::load(params.config_file()).unwrap_or_default(); - settings::start_settings_dialog(sender.clone(), Arc::new(params)); - } - "exit" => { - let _ = sender.send_blocking(TrayCommand::Exit); - glib::idle_add(|| { - gtk::main_quit(); - ControlFlow::Break - }); - } - "about" => { - glib::idle_add(|| { - let dialog = gtk::AboutDialog::builder() - .version(env!("CARGO_PKG_VERSION")) - .logo_icon_name("network-vpn") - .website("https://github.com/ancwrd1/snx-rs") - .authors(["Dmitry Pankratov"]) - .license_type(License::Agpl30) - .program_name("SNX-RS VPN Client for Linux") - .title("SNX-RS VPN Client for Linux") - .build(); - - dialog.run(); - dialog.close(); - - ControlFlow::Break - }); - } - _ => {} + if tunnel_params.ike_persist { + let _ = sender.send(TrayCommand::Service(ServiceCommand::Connect)).await; + } + + tokio::spawn(async move { + while let Some(v) = event_receiver.recv().await { + match v { + "connect" => { + let _ = sender.send(TrayCommand::Service(ServiceCommand::Connect)).await; + } + "disconnect" => { + let _ = sender.send(TrayCommand::Service(ServiceCommand::Disconnect)).await; } + "settings" => { + let params = TunnelParams::load(params.config_file()).unwrap_or_default(); + settings::start_settings_dialog(sender.clone(), Arc::new(params)); + } + "exit" => { + let _ = sender.send(TrayCommand::Exit).await; + glib::idle_add(|| { + gtk::main_quit(); + ControlFlow::Break + }); + } + "about" => { + glib::idle_add(|| { + let dialog = gtk::AboutDialog::builder() + .version(env!("CARGO_PKG_VERSION")) + .logo_icon_name("network-vpn") + .website("https://github.com/ancwrd1/snx-rs") + .authors(["Dmitry Pankratov"]) + .license_type(License::Agpl30) + .program_name("SNX-RS VPN Client for Linux") + .title("SNX-RS VPN Client for Linux") + .build(); + + dialog.run(); + dialog.close(); + + ControlFlow::Break + }); + } + _ => {} } - }); - - glib::spawn_future_local(async move { my_tray.run().await }); - - gtk::main(); + } }); app.run_with_args::<&str>(&[]); diff --git a/snx-rs-gui/src/settings.rs b/snx-rs-gui/src/settings.rs index f52c10f..63160b8 100644 --- a/snx-rs-gui/src/settings.rs +++ b/snx-rs-gui/src/settings.rs @@ -1,5 +1,4 @@ use anyhow::anyhow; -use async_channel::Sender; use gtk::{ glib::{self, clone}, prelude::*, @@ -8,6 +7,7 @@ use gtk::{ use ipnet::Ipv4Net; use std::net::Ipv4Addr; use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; +use tokio::sync::mpsc::Sender; use tracing::warn; use crate::tray::TrayCommand; @@ -311,12 +311,8 @@ impl SettingsDialog { ..(*params2).clone() }; glib::spawn_future_local(clone!(@strong sender => async move { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - let response = rt - .spawn(async move { server_info::get(¶ms).await }) + let response = tokio:: + spawn(async move { server_info::get(¶ms).await }) .await .unwrap(); let _ = sender.send(response).await; @@ -833,7 +829,8 @@ pub fn start_settings_dialog(sender: Sender, params: Arc {} diff --git a/snx-rs-gui/src/theme.rs b/snx-rs-gui/src/theme.rs index cf04385..d2db68c 100644 --- a/snx-rs-gui/src/theme.rs +++ b/snx-rs-gui/src/theme.rs @@ -2,8 +2,6 @@ use std::sync::atomic::{AtomicU32, Ordering}; use anyhow::anyhow; use futures::StreamExt; -use once_cell::sync::Lazy; -use tokio::runtime::Runtime; use tracing::debug; use zbus::Connection; @@ -42,36 +40,27 @@ pub fn system_color_theme() -> anyhow::Result { COLOR_THEME.load(Ordering::SeqCst).try_into() } -pub fn init_theme_monitoring() -> anyhow::Result<()> { - static RT: Lazy = Lazy::new(|| { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - }); - - RT.block_on(async { - let connection = Connection::session().await?; - let proxy = DesktopSettingsProxy::new(&connection).await?; - let scheme = proxy.read_one("org.freedesktop.appearance", "color-scheme").await?; - let scheme = u32::try_from(scheme)?; - COLOR_THEME.store(scheme, Ordering::SeqCst); +pub async fn init_theme_monitoring() -> anyhow::Result<()> { + let connection = Connection::session().await?; + let proxy = DesktopSettingsProxy::new(&connection).await?; + let scheme = proxy.read_one("org.freedesktop.appearance", "color-scheme").await?; + let scheme = u32::try_from(scheme)?; + COLOR_THEME.store(scheme, Ordering::SeqCst); - debug!("System color scheme: {}", scheme); + debug!("System color scheme: {}", scheme); - tokio::spawn(async move { - let mut stream = proxy.receive_setting_changed().await?; - while let Some(signal) = stream.next().await { - let args = signal.args()?; - if args.namespace == "org.freedesktop.appearance" && args.key == "color-scheme" { - let scheme = u32::try_from(args.value)?; - debug!("New system color scheme: {}", scheme); - COLOR_THEME.store(scheme, Ordering::SeqCst); - } + tokio::spawn(async move { + let mut stream = proxy.receive_setting_changed().await?; + while let Some(signal) = stream.next().await { + let args = signal.args()?; + if args.namespace == "org.freedesktop.appearance" && args.key == "color-scheme" { + let scheme = u32::try_from(args.value)?; + debug!("New system color scheme: {}", scheme); + COLOR_THEME.store(scheme, Ordering::SeqCst); } - Ok::<_, anyhow::Error>(()) - }); + } + Ok::<_, anyhow::Error>(()) + }); - Ok(()) - }) + Ok(()) } diff --git a/snx-rs-gui/src/tray.rs b/snx-rs-gui/src/tray.rs index 38b49d6..7a02c8f 100644 --- a/snx-rs-gui/src/tray.rs +++ b/snx-rs-gui/src/tray.rs @@ -1,13 +1,8 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::anyhow; -use async_channel::{Receiver, Sender}; -use tray_icon::{ - menu::{ContextMenu, Menu, MenuItem, PredefinedMenuItem}, - Icon, TrayIcon, TrayIconBuilder, -}; - -use crate::{assets, params::CmdlineParams, prompt, theme::system_color_theme, theme::SystemColorTheme}; +use ksni::{menu::StandardItem, Icon, MenuItem, TrayMethods}; +use tokio::sync::mpsc::{Receiver, Sender}; use snxcore::{ browser::BrowserController, @@ -17,6 +12,8 @@ use snxcore::{ prompt::SecurePrompt, }; +use crate::{assets, params::CmdlineParams, prompt, theme::system_color_theme, theme::SystemColorTheme}; + const TITLE: &str = "SNX-RS VPN client"; fn browser(_params: Arc) -> impl BrowserController { @@ -33,39 +30,18 @@ pub enum TrayCommand { pub struct AppTray { command_sender: Sender, command_receiver: Option>, + event_sender: Sender<&'static str>, + config_file: PathBuf, +} + +struct KsniTrayImpl { + event_sender: Sender<&'static str>, status: anyhow::Result, connecting: bool, config_file: PathBuf, - tray_icon: TrayIcon, } -impl AppTray { - pub fn new(params: &CmdlineParams) -> anyhow::Result { - let (tx, rx) = async_channel::bounded(256); - - let tray_icon = TrayIconBuilder::new() - .with_tooltip(TITLE) - .with_menu_on_left_click(true) - .build()?; - - let app_tray = AppTray { - command_sender: tx, - command_receiver: Some(rx), - status: Err(anyhow!("No service connection")), - connecting: false, - config_file: params.config_file().clone(), - tray_icon, - }; - - app_tray.update()?; - - Ok(app_tray) - } - - pub fn sender(&self) -> Sender { - self.command_sender.clone() - } - +impl KsniTrayImpl { fn status_label(&self) -> String { if self.connecting { "...".to_owned() @@ -87,33 +63,60 @@ impl AppTray { } } - fn menu(&self) -> anyhow::Result> { - let menu = Menu::new(); - menu.append(&MenuItem::new(self.status_label(), false, None))?; - menu.append(&PredefinedMenuItem::separator())?; - menu.append(&MenuItem::with_id( - "connect", - "Connect", - self.status - .as_ref() - .is_ok_and(|status| status.connected_since.is_none() && status.mfa.is_none()) - && !self.connecting, - None, - ))?; - menu.append(&MenuItem::with_id( - "disconnect", - "Disconnect", - self.status - .as_ref() - .is_ok_and(|status| status.connected_since.is_some()), - None, - ))?; - - menu.append(&MenuItem::with_id("settings", "Settings...", true, None))?; - menu.append(&MenuItem::with_id("about", "About...", true, None))?; - menu.append(&MenuItem::with_id("exit", "Exit", true, None))?; - - Ok(Box::new(menu)) + fn send_event(&mut self, event: &'static str) { + let sender = self.event_sender.clone(); + tokio::spawn(async move { sender.send(event).await }); + } + + fn create_menu(&self) -> Vec> { + vec![ + StandardItem { + label: self.status_label(), + enabled: false, + ..Default::default() + } + .into(), + MenuItem::Separator, + StandardItem { + label: "Connect".to_string(), + enabled: self + .status + .as_ref() + .is_ok_and(|status| status.connected_since.is_none() && status.mfa.is_none()) + && !self.connecting, + activate: Box::new(|tray: &mut KsniTrayImpl| tray.send_event("connect")), + ..Default::default() + } + .into(), + StandardItem { + label: "Disconnect".to_string(), + enabled: self + .status + .as_ref() + .is_ok_and(|status| status.connected_since.is_some()), + activate: Box::new(|tray: &mut KsniTrayImpl| tray.send_event("disconnect")), + ..Default::default() + } + .into(), + StandardItem { + label: "Settings...".to_string(), + activate: Box::new(|tray: &mut KsniTrayImpl| tray.send_event("settings")), + ..Default::default() + } + .into(), + StandardItem { + label: "About...".to_string(), + activate: Box::new(|tray: &mut KsniTrayImpl| tray.send_event("about")), + ..Default::default() + } + .into(), + StandardItem { + label: "Exit".to_string(), + activate: Box::new(|tray: &mut KsniTrayImpl| tray.send_event("exit")), + ..Default::default() + } + .into(), + ] } fn icon_theme(&self) -> &'static assets::IconTheme { @@ -132,7 +135,7 @@ impl AppTray { } } - fn icon(&self) -> anyhow::Result { + fn create_icon(&self) -> Icon { let theme = self.icon_theme(); let data = if self.connecting { @@ -150,32 +153,73 @@ impl AppTray { } }; - Ok(Icon::from_rgba(data, 256, 256)?) + Icon { + width: 256, + height: 256, + data, + } } +} - fn update(&self) -> anyhow::Result<()> { - self.tray_icon.set_icon(Some(self.icon()?))?; - self.tray_icon.set_menu(Some(self.menu()?)); - Ok(()) +impl ksni::Tray for KsniTrayImpl { + const MENU_ON_ACTIVATE: bool = true; + fn id(&self) -> String { + env!("CARGO_PKG_NAME").into() + } + fn title(&self) -> String { + TITLE.into() + } + + fn icon_pixmap(&self) -> Vec { + vec![self.create_icon()] + } + + fn menu(&self) -> Vec> { + self.create_menu() + } +} + +impl AppTray { + pub fn new(params: &CmdlineParams, event_sender: Sender<&'static str>) -> Self { + let (tx, rx) = tokio::sync::mpsc::channel(256); + + AppTray { + command_sender: tx, + command_receiver: Some(rx), + event_sender, + config_file: params.config_file().clone(), + } + } + + pub fn sender(&self) -> Sender { + self.command_sender.clone() } pub async fn run(&mut self) -> anyhow::Result<()> { + let tray_impl = KsniTrayImpl { + event_sender: self.event_sender.clone(), + status: Err(anyhow!("No service connection")), + connecting: false, + config_file: self.config_file.clone(), + }; + + let handle = tray_impl.spawn().await?; + let mut prev_command = ServiceCommand::Info; let mut prev_status = String::new(); let mut prev_theme = None; - let rx = self.command_receiver.take().unwrap(); - - let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; + let mut rx = self.command_receiver.take().unwrap(); - while let Ok(command) = rx.recv().await { + while let Some(command) = rx.recv().await { let command = match command { TrayCommand::Service(command) => command, TrayCommand::Update => { - self.update()?; + handle.update(|_| ()).await; continue; } TrayCommand::Exit => { + handle.shutdown().await; break; } }; @@ -183,7 +227,7 @@ impl AppTray { let theme = system_color_theme().ok(); if theme != prev_theme { prev_theme = theme; - self.update()?; + handle.update(|_| ()).await; } let tunnel_params = Arc::new(TunnelParams::load(&self.config_file).unwrap_or_default()); @@ -192,11 +236,10 @@ impl AppTray { ServiceController::new(prompt::GtkPrompt, browser(tunnel_params.clone()), tunnel_params) { if command == ServiceCommand::Connect { - self.connecting = true; - self.update()?; + handle.update(|tray| tray.connecting = true).await; } - let result = rt.spawn(async move { controller.command(command).await }).await; + let result = tokio::spawn(async move { controller.command(command).await }).await; let status = match result { Ok(result) => result, @@ -213,9 +256,12 @@ impl AppTray { } if command != prev_command || status_str != prev_status { - self.connecting = false; - self.status = status; - self.update()?; + handle + .update(|tray| { + tray.connecting = false; + tray.status = status; + }) + .await; } prev_command = command; prev_status = status_str;