gardesk/garclip / 1194921

Browse files

daemon: add state machine and event loop

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
11949214e5001a91d500decee356a5870ddbcc41
Parents
fd9299c
Tree
32c66f2

2 changed files

StatusFile+-
A garclip/src/daemon/mod.rs 3 0
A garclip/src/daemon/state.rs 362 0
garclip/src/daemon/mod.rsadded
@@ -0,0 +1,3 @@
1
+pub mod state;
2
+
3
+pub use state::DaemonState;
garclip/src/daemon/state.rsadded
@@ -0,0 +1,362 @@
1
+use std::sync::Arc;
2
+use std::time::{Duration, Instant};
3
+
4
+use base64::{engine::general_purpose::STANDARD, Engine};
5
+use tokio::sync::mpsc;
6
+use x11rb::connection::Connection;
7
+use x11rb::protocol::xproto::{SelectionClearEvent, SelectionRequestEvent};
8
+use x11rb::protocol::Event as X11Event;
9
+use x11rb::rust_connection::RustConnection;
10
+
11
+use crate::clipboard::{ClipboardContent, ClipboardHistory, ClipboardManager};
12
+use crate::config::Config;
13
+use crate::error::Result;
14
+use crate::ipc::protocol::{Command, EntryInfo, Event, PasteResponse, Response, StatusInfo};
15
+
16
+/// Main daemon state
17
+pub struct DaemonState {
18
+    config: Config,
19
+    manager: ClipboardManager,
20
+    start_time: Instant,
21
+    event_tx: mpsc::Sender<Event>,
22
+}
23
+
24
+impl DaemonState {
25
+    /// Create a new daemon state
26
+    pub fn new(config: Config, event_tx: mpsc::Sender<Event>) -> Result<Self> {
27
+        // Connect to X11
28
+        let (conn, screen_num) = RustConnection::connect(None)?;
29
+        let conn = Arc::new(conn);
30
+
31
+        // Load or create history
32
+        let history_path = config.history_path();
33
+        let history = if config.history.persist && history_path.exists() {
34
+            tracing::info!("Loading history from {:?}", history_path);
35
+            ClipboardHistory::load_or_new(
36
+                &history_path,
37
+                config.history.max_entries,
38
+                config.behavior.deduplicate,
39
+            )?
40
+        } else {
41
+            ClipboardHistory::new(config.history.max_entries, config.behavior.deduplicate)
42
+        };
43
+
44
+        // Create clipboard manager
45
+        let manager = ClipboardManager::new(
46
+            conn,
47
+            screen_num,
48
+            history,
49
+            config.behavior.watch_primary,
50
+        )?;
51
+
52
+        Ok(Self {
53
+            config,
54
+            manager,
55
+            start_time: Instant::now(),
56
+            event_tx,
57
+        })
58
+    }
59
+
60
+    /// Get the config
61
+    pub fn config(&self) -> &Config {
62
+        &self.config
63
+    }
64
+
65
+    /// Reload configuration
66
+    pub fn reload_config(&mut self) -> Result<()> {
67
+        self.config = Config::load_default();
68
+        tracing::info!("Configuration reloaded");
69
+        Ok(())
70
+    }
71
+
72
+    /// Poll for clipboard changes
73
+    pub fn poll_clipboard(&mut self) -> Result<()> {
74
+        // Poll CLIPBOARD
75
+        if self.config.behavior.watch_clipboard {
76
+            if let Some(id) = self.manager.poll_clipboard()? {
77
+                if let Some(entry) = self.manager.history().get(id) {
78
+                    let content_type = if entry.content.is_text() {
79
+                        "text"
80
+                    } else {
81
+                        "image"
82
+                    };
83
+
84
+                    let event = Event::ClipboardChanged {
85
+                        id,
86
+                        preview: entry.preview(100),
87
+                        source: entry.source.clone(),
88
+                        content_type: content_type.to_string(),
89
+                    };
90
+
91
+                    // Send event (non-blocking)
92
+                    let _ = self.event_tx.try_send(event);
93
+                }
94
+            }
95
+        }
96
+
97
+        // Poll PRIMARY
98
+        if self.config.behavior.watch_primary {
99
+            if let Some(id) = self.manager.poll_primary()? {
100
+                if let Some(entry) = self.manager.history().get(id) {
101
+                    let content_type = if entry.content.is_text() {
102
+                        "text"
103
+                    } else {
104
+                        "image"
105
+                    };
106
+
107
+                    let event = Event::ClipboardChanged {
108
+                        id,
109
+                        preview: entry.preview(100),
110
+                        source: entry.source.clone(),
111
+                        content_type: content_type.to_string(),
112
+                    };
113
+
114
+                    let _ = self.event_tx.try_send(event);
115
+                }
116
+            }
117
+        }
118
+
119
+        Ok(())
120
+    }
121
+
122
+    /// Process X11 events
123
+    pub fn process_x11_events(&mut self) -> Result<()> {
124
+        let conn = self.manager.conn().clone();
125
+
126
+        while let Ok(Some(event)) = conn.poll_for_event() {
127
+            match event {
128
+                X11Event::SelectionRequest(req) => {
129
+                    self.handle_selection_request(&req)?;
130
+                }
131
+                X11Event::SelectionClear(clear) => {
132
+                    self.handle_selection_clear(&clear);
133
+                }
134
+                _ => {}
135
+            }
136
+        }
137
+
138
+        Ok(())
139
+    }
140
+
141
+    /// Handle a selection request
142
+    fn handle_selection_request(&self, event: &SelectionRequestEvent) -> Result<()> {
143
+        tracing::trace!(
144
+            "SelectionRequest: selection={}, target={}, requestor={}",
145
+            event.selection,
146
+            event.target,
147
+            event.requestor
148
+        );
149
+        self.manager.handle_selection_request(event)?;
150
+        Ok(())
151
+    }
152
+
153
+    /// Handle selection clear
154
+    fn handle_selection_clear(&mut self, event: &SelectionClearEvent) {
155
+        tracing::trace!("SelectionClear: selection={}", event.selection);
156
+        self.manager.handle_selection_clear(event.selection);
157
+    }
158
+
159
+    /// Handle an IPC command
160
+    pub async fn handle_command(&mut self, cmd: Command) -> Response {
161
+        match cmd {
162
+            Command::Copy { text } => self.cmd_copy(text),
163
+            Command::CopyImage { data, mime_type } => self.cmd_copy_image(data, mime_type),
164
+            Command::Paste => self.cmd_paste(),
165
+            Command::History { limit } => self.cmd_history(limit),
166
+            Command::Select { id } => self.cmd_select(id),
167
+            Command::Delete { id } => self.cmd_delete(id),
168
+            Command::Clear => self.cmd_clear(),
169
+            Command::ClearHistory { keep_pinned } => self.cmd_clear_history(keep_pinned),
170
+            Command::Pin { id } => self.cmd_pin(id),
171
+            Command::Unpin { id } => self.cmd_unpin(id),
172
+            Command::ListPinned => self.cmd_list_pinned(),
173
+            Command::Search { query, limit } => self.cmd_search(query, limit),
174
+            Command::Status => self.cmd_status(),
175
+            Command::Reload => self.cmd_reload(),
176
+            Command::Quit => Response::ok(), // Handled by caller
177
+            Command::Subscribe { .. } => Response::ok(), // Handled by caller
178
+        }
179
+    }
180
+
181
+    fn cmd_copy(&mut self, text: String) -> Response {
182
+        let content = ClipboardContent::Text(text);
183
+        match self.manager.set_clipboard(content) {
184
+            Ok(_) => Response::ok(),
185
+            Err(e) => Response::err(e.to_string()),
186
+        }
187
+    }
188
+
189
+    fn cmd_copy_image(&mut self, data: String, mime_type: String) -> Response {
190
+        match STANDARD.decode(&data) {
191
+            Ok(bytes) => {
192
+                let content = ClipboardContent::Image {
193
+                    data: bytes,
194
+                    mime_type,
195
+                };
196
+                match self.manager.set_clipboard(content) {
197
+                    Ok(_) => Response::ok(),
198
+                    Err(e) => Response::err(e.to_string()),
199
+                }
200
+            }
201
+            Err(e) => Response::err(format!("Invalid base64: {}", e)),
202
+        }
203
+    }
204
+
205
+    fn cmd_paste(&self) -> Response {
206
+        if let Some(entry) = self.manager.history().current() {
207
+            let response = match &entry.content {
208
+                ClipboardContent::Text(text) => PasteResponse {
209
+                    id: entry.id,
210
+                    content_type: "text".to_string(),
211
+                    text: Some(text.clone()),
212
+                    image_data: None,
213
+                    mime_type: None,
214
+                },
215
+                ClipboardContent::Image { data, mime_type } => PasteResponse {
216
+                    id: entry.id,
217
+                    content_type: "image".to_string(),
218
+                    text: None,
219
+                    image_data: Some(STANDARD.encode(data)),
220
+                    mime_type: Some(mime_type.clone()),
221
+                },
222
+            };
223
+            Response::ok_with_data(response)
224
+        } else {
225
+            Response::err("Clipboard is empty")
226
+        }
227
+    }
228
+
229
+    fn cmd_history(&self, limit: usize) -> Response {
230
+        let entries: Vec<EntryInfo> = self
231
+            .manager
232
+            .history()
233
+            .list(limit)
234
+            .into_iter()
235
+            .map(EntryInfo::from)
236
+            .collect();
237
+        Response::ok_with_data(entries)
238
+    }
239
+
240
+    fn cmd_select(&mut self, id: u64) -> Response {
241
+        match self.manager.select_entry(id) {
242
+            Ok(true) => {
243
+                let _ = self.event_tx.try_send(Event::EntrySelected { id });
244
+                Response::ok()
245
+            }
246
+            Ok(false) => Response::err(format!("Entry {} not found", id)),
247
+            Err(e) => Response::err(e.to_string()),
248
+        }
249
+    }
250
+
251
+    fn cmd_delete(&mut self, id: u64) -> Response {
252
+        if self.manager.history_mut().remove(id) {
253
+            let _ = self.event_tx.try_send(Event::EntryDeleted { id });
254
+            Response::ok()
255
+        } else {
256
+            Response::err(format!("Entry {} not found", id))
257
+        }
258
+    }
259
+
260
+    fn cmd_clear(&mut self) -> Response {
261
+        match self.manager.clear_clipboard() {
262
+            Ok(_) => Response::ok(),
263
+            Err(e) => Response::err(e.to_string()),
264
+        }
265
+    }
266
+
267
+    fn cmd_clear_history(&mut self, keep_pinned: bool) -> Response {
268
+        self.manager.history_mut().clear(keep_pinned);
269
+        let _ = self.event_tx.try_send(Event::HistoryCleared);
270
+        Response::ok()
271
+    }
272
+
273
+    fn cmd_pin(&mut self, id: u64) -> Response {
274
+        if self.manager.history_mut().pin(id) {
275
+            let _ = self.event_tx.try_send(Event::EntryPinned { id });
276
+            Response::ok()
277
+        } else {
278
+            Response::err(format!("Entry {} not found", id))
279
+        }
280
+    }
281
+
282
+    fn cmd_unpin(&mut self, id: u64) -> Response {
283
+        if self.manager.history_mut().unpin(id) {
284
+            let _ = self.event_tx.try_send(Event::EntryUnpinned { id });
285
+            Response::ok()
286
+        } else {
287
+            Response::err(format!("Entry {} not found", id))
288
+        }
289
+    }
290
+
291
+    fn cmd_list_pinned(&self) -> Response {
292
+        let entries: Vec<EntryInfo> = self
293
+            .manager
294
+            .history()
295
+            .list_pinned()
296
+            .into_iter()
297
+            .map(EntryInfo::from)
298
+            .collect();
299
+        Response::ok_with_data(entries)
300
+    }
301
+
302
+    fn cmd_search(&self, query: String, limit: usize) -> Response {
303
+        let entries: Vec<EntryInfo> = self
304
+            .manager
305
+            .history()
306
+            .search(&query, limit)
307
+            .into_iter()
308
+            .map(EntryInfo::from)
309
+            .collect();
310
+        Response::ok_with_data(entries)
311
+    }
312
+
313
+    fn cmd_status(&self) -> Response {
314
+        let history = self.manager.history();
315
+        let owns_clipboard = self.manager.owns_clipboard().unwrap_or(false);
316
+
317
+        let (current_preview, current_type) = if let Some(entry) = history.current() {
318
+            let ctype = if entry.content.is_text() {
319
+                "text"
320
+            } else {
321
+                "image"
322
+            };
323
+            (Some(entry.preview(100)), Some(ctype.to_string()))
324
+        } else {
325
+            (None, None)
326
+        };
327
+
328
+        let status = StatusInfo {
329
+            history_count: history.len(),
330
+            pinned_count: history.list_pinned().len(),
331
+            owns_clipboard,
332
+            watching_primary: self.config.behavior.watch_primary,
333
+            current_preview,
334
+            current_type,
335
+            uptime_secs: self.start_time.elapsed().as_secs(),
336
+        };
337
+
338
+        Response::ok_with_data(status)
339
+    }
340
+
341
+    fn cmd_reload(&mut self) -> Response {
342
+        match self.reload_config() {
343
+            Ok(_) => Response::ok(),
344
+            Err(e) => Response::err(e.to_string()),
345
+        }
346
+    }
347
+
348
+    /// Save history to disk
349
+    pub fn save_history(&self) -> Result<()> {
350
+        if self.config.history.persist {
351
+            let path = self.config.history_path();
352
+            tracing::info!("Saving history to {:?}", path);
353
+            self.manager.save_history(&path)?;
354
+        }
355
+        Ok(())
356
+    }
357
+
358
+    /// Get the poll interval
359
+    pub fn poll_interval(&self) -> Duration {
360
+        Duration::from_millis(self.config.behavior.poll_interval_ms)
361
+    }
362
+}