mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-21 14:27:14 +00:00
Fixed tray icon on linux
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2683,6 +2683,7 @@ dependencies = [
|
|||||||
"egui",
|
"egui",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
|
"glib",
|
||||||
"gtk",
|
"gtk",
|
||||||
"hostname",
|
"hostname",
|
||||||
"log",
|
"log",
|
||||||
|
@@ -49,12 +49,13 @@ urlencoding = "2.1"
|
|||||||
# Linux-specific dependencies for GTK tray support
|
# Linux-specific dependencies for GTK tray support
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
gtk = { version = "0.18", optional = true }
|
gtk = { version = "0.18", optional = true }
|
||||||
|
glib = { version = "0.18", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["server", "web", "gui"]
|
default = ["server", "web", "gui"]
|
||||||
cli = ["server", "web"]
|
cli = ["server", "web"]
|
||||||
desktop = ["gui"]
|
desktop = ["gui"]
|
||||||
gui = ["tray-icon", "eframe", "egui", "winit", "notify", "notify-debouncer-mini", "gtk"]
|
gui = ["tray-icon", "eframe", "egui", "winit", "notify", "notify-debouncer-mini", "gtk", "glib"]
|
||||||
server = []
|
server = []
|
||||||
web = []
|
web = []
|
||||||
|
|
||||||
|
@@ -15,10 +15,46 @@ use winit::{
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use winit::platform::macos::EventLoopBuilderExtMacOS;
|
use winit::platform::macos::EventLoopBuilderExtMacOS;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use gtk::glib;
|
||||||
|
|
||||||
// GTK initialization for Linux tray support
|
// GTK initialization for Linux tray support
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
static GTK_INIT: std::sync::Once = std::sync::Once::new();
|
static GTK_INIT: std::sync::Once = std::sync::Once::new();
|
||||||
|
|
||||||
|
// Channel for Linux tray communication
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
type LinuxTrayChannel = (
|
||||||
|
std::sync::mpsc::Sender<LinuxTrayCommand>,
|
||||||
|
std::sync::mpsc::Receiver<LinuxTrayResponse>,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
enum LinuxTrayCommand {
|
||||||
|
CreateTray {
|
||||||
|
settings: KhmSettings,
|
||||||
|
sync_status: SyncStatus,
|
||||||
|
},
|
||||||
|
UpdateMenu {
|
||||||
|
settings: KhmSettings,
|
||||||
|
},
|
||||||
|
SetTooltip {
|
||||||
|
tooltip: String,
|
||||||
|
},
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
enum LinuxTrayResponse {
|
||||||
|
TrayCreated {
|
||||||
|
menu_ids: TrayMenuIds,
|
||||||
|
},
|
||||||
|
MenuUpdated {
|
||||||
|
menu_ids: TrayMenuIds,
|
||||||
|
},
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
create_tooltip, create_tray_icon, start_auto_sync_task, update_sync_status, update_tray_menu,
|
create_tooltip, create_tray_icon, start_auto_sync_task, update_sync_status, update_tray_menu,
|
||||||
SyncStatus, TrayMenuIds,
|
SyncStatus, TrayMenuIds,
|
||||||
@@ -26,7 +62,12 @@ use super::{
|
|||||||
use crate::gui::common::{get_config_path, load_settings, perform_sync, KhmSettings};
|
use crate::gui::common::{get_config_path, load_settings, perform_sync, KhmSettings};
|
||||||
|
|
||||||
pub struct TrayApplication {
|
pub struct TrayApplication {
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
tray_icon: Option<TrayIcon>,
|
tray_icon: Option<TrayIcon>,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
linux_tray_tx: Option<std::sync::mpsc::Sender<LinuxTrayCommand>>,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
linux_tray_handle: Option<std::thread::JoinHandle<()>>,
|
||||||
menu_ids: Option<TrayMenuIds>,
|
menu_ids: Option<TrayMenuIds>,
|
||||||
settings: Arc<Mutex<KhmSettings>>,
|
settings: Arc<Mutex<KhmSettings>>,
|
||||||
sync_status: Arc<Mutex<SyncStatus>>,
|
sync_status: Arc<Mutex<SyncStatus>>,
|
||||||
@@ -39,7 +80,12 @@ pub struct TrayApplication {
|
|||||||
impl TrayApplication {
|
impl TrayApplication {
|
||||||
pub fn new(proxy: EventLoopProxy<crate::gui::UserEvent>) -> Self {
|
pub fn new(proxy: EventLoopProxy<crate::gui::UserEvent>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
tray_icon: None,
|
tray_icon: None,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
linux_tray_tx: None,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
linux_tray_handle: None,
|
||||||
menu_ids: None,
|
menu_ids: None,
|
||||||
settings: Arc::new(Mutex::new(load_settings())),
|
settings: Arc::new(Mutex::new(load_settings())),
|
||||||
sync_status: Arc::new(Mutex::new(SyncStatus::default())),
|
sync_status: Arc::new(Mutex::new(SyncStatus::default())),
|
||||||
@@ -94,12 +140,19 @@ impl TrayApplication {
|
|||||||
*self.settings.lock().unwrap() = new_settings;
|
*self.settings.lock().unwrap() = new_settings;
|
||||||
|
|
||||||
// Update menu
|
// Update menu
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
if let Some(tray_icon) = &self.tray_icon {
|
if let Some(tray_icon) = &self.tray_icon {
|
||||||
let settings = self.settings.lock().unwrap();
|
let settings = self.settings.lock().unwrap();
|
||||||
let new_menu_ids = update_tray_menu(tray_icon, &settings);
|
let new_menu_ids = update_tray_menu(tray_icon, &settings);
|
||||||
self.menu_ids = Some(new_menu_ids);
|
self.menu_ids = Some(new_menu_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if let Some(ref tx) = self.linux_tray_tx {
|
||||||
|
let settings = self.settings.lock().unwrap().clone();
|
||||||
|
let _ = tx.send(LinuxTrayCommand::UpdateMenu { settings });
|
||||||
|
}
|
||||||
|
|
||||||
// Update tooltip
|
// Update tooltip
|
||||||
self.update_tooltip();
|
self.update_tooltip();
|
||||||
|
|
||||||
@@ -127,12 +180,19 @@ impl TrayApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_tooltip(&self) {
|
fn update_tooltip(&self) {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
let sync_status = self.sync_status.lock().unwrap();
|
||||||
|
let tooltip = create_tooltip(&settings, &sync_status);
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
if let Some(tray_icon) = &self.tray_icon {
|
if let Some(tray_icon) = &self.tray_icon {
|
||||||
let settings = self.settings.lock().unwrap();
|
|
||||||
let sync_status = self.sync_status.lock().unwrap();
|
|
||||||
let tooltip = create_tooltip(&settings, &sync_status);
|
|
||||||
let _ = tray_icon.set_tooltip(Some(&tooltip));
|
let _ = tray_icon.set_tooltip(Some(&tooltip));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if let Some(ref tx) = self.linux_tray_tx {
|
||||||
|
let _ = tx.send(LinuxTrayCommand::SetTooltip { tooltip });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_menu_event(
|
fn handle_menu_event(
|
||||||
@@ -225,17 +285,9 @@ impl ApplicationHandler<crate::gui::UserEvent> for TrayApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
|
fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
if self.tray_icon.is_none() {
|
if self.tray_icon.is_none() {
|
||||||
info!("Creating tray icon");
|
info!("Creating tray icon");
|
||||||
|
|
||||||
// Initialize GTK on Linux before creating tray icon
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
GTK_INIT.call_once(|| {
|
|
||||||
if let Err(e) = gtk::init() {
|
|
||||||
error!("Failed to initialize GTK: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let settings = self.settings.lock().unwrap();
|
let settings = self.settings.lock().unwrap();
|
||||||
let sync_status = self.sync_status.lock().unwrap();
|
let sync_status = self.sync_status.lock().unwrap();
|
||||||
|
|
||||||
@@ -255,15 +307,108 @@ impl ApplicationHandler<crate::gui::UserEvent> for TrayApplication {
|
|||||||
drop(settings);
|
drop(settings);
|
||||||
drop(sync_status);
|
drop(sync_status);
|
||||||
error!("Failed to create tray icon. This usually means the required system libraries are not installed.");
|
error!("Failed to create tray icon. This usually means the required system libraries are not installed.");
|
||||||
error!("On Ubuntu/Debian, try installing: sudo apt install libayatana-appindicator3-1");
|
|
||||||
error!("Alternative: sudo apt install libappindicator3-1");
|
|
||||||
error!("KHM will exit as system tray integration is required for desktop mode.");
|
error!("KHM will exit as system tray integration is required for desktop mode.");
|
||||||
|
|
||||||
// Exit if tray icon creation fails
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if self.linux_tray_tx.is_none() {
|
||||||
|
info!("Creating tray icon on Linux");
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let (response_tx, response_rx) = std::sync::mpsc::channel();
|
||||||
|
self.linux_tray_tx = Some(tx.clone());
|
||||||
|
|
||||||
|
let proxy = self.proxy.clone();
|
||||||
|
|
||||||
|
// Spawn GTK thread for tray
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
if let Err(e) = gtk::init() {
|
||||||
|
error!("Failed to initialize GTK: {}", e);
|
||||||
|
let _ = response_tx.send(LinuxTrayResponse::Error(format!("GTK init failed: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tray_icon: Option<TrayIcon> = None;
|
||||||
|
|
||||||
|
// Set up GTK event handlers
|
||||||
|
let tx_clone = tx.clone();
|
||||||
|
glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
|
||||||
|
while let Ok(cmd) = rx.try_recv() {
|
||||||
|
match cmd {
|
||||||
|
LinuxTrayCommand::CreateTray { settings, sync_status } => {
|
||||||
|
match std::panic::catch_unwind(|| create_tray_icon(&settings, &sync_status)) {
|
||||||
|
Ok((icon, menu_ids)) => {
|
||||||
|
tray_icon = Some(icon);
|
||||||
|
let _ = response_tx.send(LinuxTrayResponse::TrayCreated { menu_ids });
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let _ = response_tx.send(LinuxTrayResponse::Error("Failed to create tray".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinuxTrayCommand::UpdateMenu { settings } => {
|
||||||
|
if let Some(ref icon) = tray_icon {
|
||||||
|
let menu_ids = update_tray_menu(icon, &settings);
|
||||||
|
let _ = response_tx.send(LinuxTrayResponse::MenuUpdated { menu_ids });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinuxTrayCommand::SetTooltip { tooltip } => {
|
||||||
|
if let Some(ref icon) = tray_icon {
|
||||||
|
let _ = icon.set_tooltip(Some(&tooltip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinuxTrayCommand::Quit => {
|
||||||
|
gtk::main_quit();
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for menu events
|
||||||
|
if let Ok(event) = MenuEvent::receiver().try_recv() {
|
||||||
|
let _ = proxy.send_event(crate::gui::UserEvent::MenuEvent(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
});
|
||||||
|
|
||||||
|
gtk::main();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.linux_tray_handle = Some(handle);
|
||||||
|
|
||||||
|
// Send command to create tray
|
||||||
|
let settings = self.settings.lock().unwrap().clone();
|
||||||
|
let sync_status = self.sync_status.lock().unwrap().clone();
|
||||||
|
|
||||||
|
if let Some(ref tx) = self.linux_tray_tx {
|
||||||
|
let _ = tx.send(LinuxTrayCommand::CreateTray { settings, sync_status });
|
||||||
|
|
||||||
|
// Wait for response
|
||||||
|
match response_rx.recv_timeout(std::time::Duration::from_secs(5)) {
|
||||||
|
Ok(LinuxTrayResponse::TrayCreated { menu_ids }) => {
|
||||||
|
self.menu_ids = Some(menu_ids);
|
||||||
|
self.setup_file_watcher();
|
||||||
|
self.start_auto_sync();
|
||||||
|
info!("KHM tray application ready");
|
||||||
|
}
|
||||||
|
Ok(LinuxTrayResponse::Error(e)) => {
|
||||||
|
error!("Failed to create tray icon: {}", e);
|
||||||
|
error!("This usually means the required system libraries are not installed.");
|
||||||
|
error!("On Ubuntu/Debian, try installing: sudo apt install libayatana-appindicator3-1");
|
||||||
|
error!("Alternative: sudo apt install libappindicator3-1");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Timeout waiting for tray creation");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_event(
|
fn user_event(
|
||||||
@@ -288,17 +433,6 @@ impl ApplicationHandler<crate::gui::UserEvent> for TrayApplication {
|
|||||||
|
|
||||||
/// Run tray application
|
/// Run tray application
|
||||||
pub async fn run_tray_app() -> std::io::Result<()> {
|
pub async fn run_tray_app() -> std::io::Result<()> {
|
||||||
// Initialize GTK early on Linux
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
if let Err(e) = gtk::init() {
|
|
||||||
return Err(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::Other,
|
|
||||||
format!("Failed to initialize GTK: {}", e),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let event_loop = {
|
let event_loop = {
|
||||||
use winit::platform::macos::ActivationPolicy;
|
use winit::platform::macos::ActivationPolicy;
|
||||||
|
Reference in New Issue
Block a user