diff --git a/Cargo.lock b/Cargo.lock index 5735c2042..8e7c37981 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4380,12 +4380,14 @@ dependencies = [ "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "gtk", "hbb_common", "hound", "impersonate_system", "include_dir", "jni", "lazy_static", + "libappindicator", "libc", "libpulse-binding", "libpulse-simple-binding", diff --git a/Cargo.toml b/Cargo.toml index f9e519545..c950c8723 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,7 +112,8 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" - +gtk = "0.15" +libappindicator = "0.7" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/build.py b/build.py index fffb22180..3e7ce853e 100755 --- a/build.py +++ b/build.py @@ -268,6 +268,9 @@ def build_flutter_arch_manjaro(version, features): def build_flutter_windows(version, features): os.system(f'cargo build --features {features} --lib --release') + if not os.path.exists("target/release/librustdesk.dll"): + print("cargo build failed, please check rust source code.") + exit(-1) os.chdir('flutter') os.system('flutter build windows --release') os.chdir('..') diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index eb9e0a483..a1c69e5a3 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -432,7 +432,8 @@ class _DesktopHomePageState extends State updateUrl = await bind.mainGetSoftwareUpdateUrl(); if (updateUrl.isNotEmpty) setState(() {}); }); - initTray(); + // disable this tray because we use tray function provided by rust now + // initTray(); trayManager.addListener(this); windowManager.addListener(this); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 5c724f59b..3af6f4fd7 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -90,10 +90,6 @@ class PlatformFFI { /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; - // if (isDesktop) { - // // TODO - // return; - // } final dylib = Platform.isAndroid ? DynamicLibrary.open('librustdesk.so') : Platform.isLinux diff --git a/src/core_main.rs b/src/core_main.rs index 46088da5c..2baff12ad 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -148,7 +148,7 @@ pub fn core_main() -> Option> { return None; } else if args[0] == "--server" { log::info!("start --server"); - #[cfg(not(target_os = "macos"))] + #[cfg(target_os = "windows")] { crate::start_server(true); return None; @@ -158,6 +158,13 @@ pub fn core_main() -> Option> { std::thread::spawn(move || crate::start_server(true)); // to-do: for flutter, starting tray not ready yet, or we can reuse sciter's tray implementation. } + #[cfg(all(target_os = "linux"))] + { + let handler = std::thread::spawn(move || crate::start_server(true)); + crate::tray::start_tray(crate::ui_interface::OPTIONS.clone()); + // revent server exit when encountering errors from tray + handler.join(); + } } else if args[0] == "--import-config" { if args.len() == 2 { let filepath; diff --git a/src/lib.rs b/src/lib.rs index a5041e9f8..58dc50b04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ mod lang; mod license; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; -#[cfg(windows)] + mod tray; mod ui_cm_interface; diff --git a/src/tray.rs b/src/tray.rs index 1f95c8306..8e9092fbf 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,8 +1,14 @@ +use hbb_common::log::{debug, error, info}; +#[cfg(target_os = "linux")] +use libappindicator::AppIndicator; +use std::env::temp_dir; use std::{ collections::HashMap, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, RwLock}, }; +#[cfg(target_os = "windows")] use trayicon::{MenuBuilder, TrayIconBuilder}; +#[cfg(target_os = "windows")] use winit::{ event::Event, event_loop::{ControlFlow, EventLoop}, @@ -15,6 +21,7 @@ enum Events { StartService, } +#[cfg(target_os = "windows")] pub fn start_tray(options: Arc>>) { let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); @@ -76,3 +83,99 @@ pub fn start_tray(options: Arc>>) { } }); } + +/// Start a tray icon in Linux +/// +/// [Block] +/// This function will block current execution, show the tray icon and handle events. +#[cfg(target_os = "linux")] +pub fn start_tray(options: Arc>>) { + use std::time::Duration; + + use gtk::traits::{GtkMenuItemExt, MenuShellExt, WidgetExt}; + info!("configuring tray"); + // init gtk context + if let Err(err) = gtk::init() { + error!("Error when starting the tray: {}", err); + return; + } + if let Some(mut appindicator) = get_default_app_indicator() { + let mut menu = gtk::Menu::new(); + let running = get_service_status(options.clone()); + // start/stop service + let label = if !running { + crate::client::translate("Start Service".to_owned()) + } else { + crate::client::translate("Stop service".to_owned()) + }; + let menu_item_service = gtk::MenuItem::with_label(label.as_str()); + menu_item_service.connect_activate(move |item| { + let lock = crate::ui_interface::SENDER.lock().unwrap(); + update_tray_service_item(options.clone(), item); + }); + menu.append(&menu_item_service); + // show tray item + menu.show_all(); + appindicator.set_menu(&mut menu); + // start event loop + info!("Setting tray event loop"); + gtk::main(); + } else { + error!("Tray process exit now"); + } +} + +#[cfg(target_os = "linux")] +fn update_tray_service_item(options: Arc>>, item: >k::MenuItem) { + use gtk::{ + traits::{GtkMenuItemExt, ListBoxRowExt}, + MenuItem, + }; + if get_service_status(options.clone()) { + debug!("Now try to stop service"); + item.set_label(&crate::client::translate("Start Service".to_owned())); + crate::ipc::set_option("stop-service", "Y"); + } else { + debug!("Now try to start service"); + item.set_label(&crate::client::translate("Stop service".to_owned())); + crate::ipc::set_option("stop-service", ""); + } +} + +#[cfg(target_os = "linux")] +fn get_default_app_indicator() -> Option { + use libappindicator::AppIndicatorStatus; + use std::io::Write; + + let icon = include_bytes!("../res/icon.png"); + // appindicator does not support icon buffer, so we write it to tmp folder + let mut icon_path = temp_dir(); + icon_path.push("RustDesk"); + icon_path.push("rustdesk.png"); + match std::fs::File::create(icon_path.clone()) { + Ok(mut f) => { + f.write_all(icon).unwrap(); + } + Err(err) => { + error!("Error when writing icon to {:?}: {}", icon_path, err); + return None; + } + } + debug!("write temp icon complete"); + let mut appindicator = AppIndicator::new("RustDesk", icon_path.to_str().unwrap_or("rustdesk")); + appindicator.set_label("RustDesk", "A remote control software."); + appindicator.set_status(AppIndicatorStatus::Active); + Some(appindicator) +} + +/// Get service status +/// Return [`true`] if service is running, [`false`] otherwise. +#[inline] +fn get_service_status(options: Arc>>) -> bool { + if let Some(v) = options.lock().unwrap().get("stop-service") { + debug!("service stopped: {}", v); + v.is_empty() + } else { + true + } +}