Skip to main content

mimick/
tray_icon.rs

1//! Integrates StatusNotifier tray functionality and provides GTK-facing control signals.
2//!
3//! Uses the `ksni` crate to register a system tray icon with menu entries
4//! for Settings, Library, Pause/Resume, Sync Now, and Quit. Each action
5//! sends a signal over a `tokio::sync::watch` channel that the GTK main
6//! loop polls to trigger the corresponding window or operation.
7
8use ksni::TrayMethods;
9use tokio::sync::watch;
10
11/// Represents the tray state shared with ksni menu callbacks and GTK main loop.
12#[derive(Debug)]
13pub struct MimickTray {
14    /// Sender used to signal the GTK main loop to open the settings window.
15    /// Sending `true` triggers the open; the receiver is polled via glib::timeout_add.
16    pub settings_tx: watch::Sender<bool>,
17    /// Sender used to signal the GTK main loop to open the library window.
18    pub library_tx: watch::Sender<bool>,
19    /// Sender used to request a graceful application quit from the GTK main loop.
20    pub quit_tx: watch::Sender<bool>,
21    /// Sender used to toggle the paused state from the tray.
22    pub pause_tx: watch::Sender<bool>,
23    /// Sender used to request an immediate catch-up scan.
24    pub sync_now_tx: watch::Sender<bool>,
25    /// Cached from `Config` at tray construction to avoid disk I/O per menu open.
26    pub library_view_enabled: bool,
27}
28
29impl ksni::Tray for MimickTray {
30    fn id(&self) -> String {
31        "mimick_tray".to_string()
32    }
33
34    fn icon_name(&self) -> String {
35        "dev.nicx.mimick".to_string()
36    }
37
38    fn title(&self) -> String {
39        "Mimick Sync".into()
40    }
41
42    fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
43        use ksni::menu::*;
44        let library_enabled = self.library_view_enabled;
45        let mut items: Vec<ksni::MenuItem<Self>> = Vec::new();
46        if library_enabled {
47            items.push(
48                StandardItem {
49                    label: "Library".into(),
50                    activate: Box::new(|tray: &mut Self| {
51                        let _ = tray.library_tx.send(true);
52                    }),
53                    ..Default::default()
54                }
55                .into(),
56            );
57        }
58        items.extend(vec![
59            StandardItem {
60                label: "Settings".into(),
61                activate: Box::new(|tray: &mut Self| {
62                    // Signal the GTK main loop — no new process spawned.
63                    let _ = tray.settings_tx.send(true);
64                }),
65                ..Default::default()
66            }
67            .into(),
68            StandardItem {
69                label: "Pause / Resume".into(),
70                activate: Box::new(|tray: &mut Self| {
71                    let _ = tray.pause_tx.send(true);
72                }),
73                ..Default::default()
74            }
75            .into(),
76            StandardItem {
77                label: "Sync Now".into(),
78                activate: Box::new(|tray: &mut Self| {
79                    let _ = tray.sync_now_tx.send(true);
80                }),
81                ..Default::default()
82            }
83            .into(),
84            MenuItem::Separator,
85            StandardItem {
86                label: "Quit".into(),
87                activate: Box::new(|tray: &mut Self| {
88                    let _ = tray.quit_tx.send(true);
89                }),
90                ..Default::default()
91            }
92            .into(),
93        ]);
94        items
95    }
96}
97
98/// Channels and handles returned by tray initialization to manage signals.
99pub struct TrayHandles {
100    /// Active ksni tray handle.
101    pub handle: ksni::Handle<MimickTray>,
102    /// Receiver for the settings-open signal.
103    pub settings_rx: watch::Receiver<bool>,
104    /// Receiver for the library-open signal.
105    pub library_rx: watch::Receiver<bool>,
106    /// Receiver for the application quit signal.
107    pub quit_rx: watch::Receiver<bool>,
108    /// Receiver for the pause/resume signal.
109    pub pause_rx: watch::Receiver<bool>,
110    /// Receiver for the catch-up sync request signal.
111    pub sync_now_rx: watch::Receiver<bool>,
112}
113
114/// Asynchronously construct and spawn the system tray icon, returning control channels.
115pub async fn build_tray(library_view_enabled: bool) -> Result<TrayHandles, ksni::Error> {
116    let (settings_tx, settings_rx) = watch::channel(false);
117    let (library_tx, library_rx) = watch::channel(false);
118    let (quit_tx, quit_rx) = watch::channel(false);
119    let (pause_tx, pause_rx) = watch::channel(false);
120    let (sync_now_tx, sync_now_rx) = watch::channel(false);
121    let tray = MimickTray {
122        settings_tx,
123        library_tx,
124        quit_tx,
125        pause_tx,
126        sync_now_tx,
127        library_view_enabled,
128    };
129    let handle = if ashpd::is_sandboxed() {
130        // Flatpak sessions already broker the item through the watcher, so we avoid owning
131        // an additional D-Bus name and keep the permission request narrower.
132        tray.disable_dbus_name(true).spawn().await?
133    } else {
134        tray.spawn().await?
135    };
136    Ok(TrayHandles {
137        handle,
138        settings_rx,
139        library_rx,
140        quit_rx,
141        pause_rx,
142        sync_now_rx,
143    })
144}