gardesk/gar / 242efe5

Browse files

feat(config): add garnotify auto-spawn integration

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
242efe581a0af45216921a554bed9eb251fd1a9d
Parents
b19cd1f
Tree
d7b12f0

5 changed files

StatusFile+-
M Cargo.lock 2 2
M gar/src/config/lua.rs 28 2
M gar/src/config/mod.rs 4 0
M gar/src/core/mod.rs 3 0
M gar/src/x11/events.rs 130 0
Cargo.lockmodified
@@ -202,7 +202,7 @@ checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
202202
 
203203
 [[package]]
204204
 name = "gar"
205
-version = "0.2.0"
205
+version = "0.3.0"
206206
 dependencies = [
207207
  "dirs",
208208
  "libc",
@@ -217,7 +217,7 @@ dependencies = [
217217
 
218218
 [[package]]
219219
 name = "garctl"
220
-version = "0.2.0"
220
+version = "0.3.0"
221221
 dependencies = [
222222
  "clap",
223223
  "serde",
gar/src/config/lua.rsmodified
@@ -117,11 +117,15 @@ impl LuaConfig {
117117
         // Check if gar.bar table exists - enables garbar integration
118118
         self.check_bar_config()?;
119119
 
120
+        // Check if gar.notification table exists - enables garnotify integration
121
+        self.check_notification_config()?;
122
+
120123
         let state = self.state.lock().unwrap();
121124
         tracing::info!(
122
-            "Config loaded: {} keybinds registered, bar_enabled={}",
125
+            "Config loaded: {} keybinds registered, bar_enabled={}, notification_enabled={}",
123126
             state.keybinds.len(),
124
-            state.config.bar_enabled
127
+            state.config.bar_enabled,
128
+            state.config.notification_enabled
125129
         );
126130
 
127131
         Ok(())
@@ -156,6 +160,28 @@ impl LuaConfig {
156160
         Ok(())
157161
     }
158162
 
163
+    /// Check if gar.notification table is configured, enabling garnotify integration
164
+    fn check_notification_config(&self) -> LuaResult<()> {
165
+        let globals = self.lua.globals();
166
+        let gar: Table = globals.get("gar")?;
167
+
168
+        // Check if gar.notification exists and is a table
169
+        match gar.get::<Table>("notification") {
170
+            Ok(_) => {
171
+                // gar.notification exists! Enable garnotify integration
172
+                let mut state = self.state.lock().unwrap();
173
+                state.config.notification_enabled = true;
174
+                tracing::info!("garnotify integration enabled");
175
+            }
176
+            Err(_) => {
177
+                // gar.notification not set, garnotify won't be auto-spawned
178
+                tracing::debug!("gar.notification not configured, garnotify integration disabled");
179
+            }
180
+        }
181
+
182
+        Ok(())
183
+    }
184
+
159185
     /// Reload configuration (clears existing keybinds and rules)
160186
     pub fn reload(&self) -> LuaResult<()> {
161187
         {
gar/src/config/mod.rsmodified
@@ -35,6 +35,8 @@ pub struct Config {
3535
     pub bar_height: u32,
3636
     // garbar integration: spawn garbar automatically if gar.bar is configured
3737
     pub bar_enabled: bool,
38
+    // garnotify integration: spawn garnotify automatically if gar.notification is configured
39
+    pub notification_enabled: bool,
3840
     // Monitor ordering: list of monitor names in desired left-to-right order
3941
     // If empty, monitors are sorted by X position (default)
4042
     pub monitor_order: Vec<String>,
@@ -476,6 +478,8 @@ impl Default for Config {
476478
             bar_height: 0,
477479
             // garbar not enabled by default (enabled when gar.bar table is set)
478480
             bar_enabled: false,
481
+            // garnotify not enabled by default (enabled when gar.notification table is set)
482
+            notification_enabled: false,
479483
             // Monitor order: empty = sort by X position
480484
             monitor_order: Vec::new(),
481485
             // Screen timeout: enabled by default with 10 minute timeout
gar/src/core/mod.rsmodified
@@ -50,6 +50,8 @@ pub struct WindowManager {
5050
     pub tiled_edge_cursor: Option<(XWindow, XWindow, Direction)>,
5151
     /// garbar child process (managed automatically when gar.bar is configured)
5252
     pub garbar_process: Option<std::process::Child>,
53
+    /// garnotify child process (managed automatically when gar.notification is configured)
54
+    pub garnotify_process: Option<std::process::Child>,
5355
     /// Directional focus memory: (source_window, direction) -> last_target_window
5456
     /// Used to remember which window was focused when navigating in a direction
5557
     pub directional_focus_memory: HashMap<(XWindow, Direction), XWindow>,
@@ -156,6 +158,7 @@ impl WindowManager {
156158
             current_edge_cursor: None,
157159
             tiled_edge_cursor: None,
158160
             garbar_process: None,
161
+            garnotify_process: None,
159162
             directional_focus_memory: HashMap::new(),
160163
         })
161164
     }
gar/src/x11/events.rsmodified
@@ -236,6 +236,125 @@ fn reload_garbar(child: &std::process::Child) {
236236
         libc::kill(child.id() as i32, libc::SIGHUP);
237237
     }
238238
 }
239
+
240
+// ============================================================================
241
+// garnotify integration - auto-spawn notification daemon
242
+// ============================================================================
243
+
244
+/// Get garnotify socket path
245
+fn garnotify_socket_path() -> String {
246
+    std::env::var("XDG_RUNTIME_DIR")
247
+        .map(|dir| format!("{}/garnotify.sock", dir))
248
+        .unwrap_or_else(|_| "/tmp/garnotify.sock".to_string())
249
+}
250
+
251
+/// Check if garnotify is healthy (socket exists)
252
+fn is_garnotify_healthy() -> bool {
253
+    std::path::Path::new(&garnotify_socket_path()).exists()
254
+}
255
+
256
+/// Spawn garnotify notification daemon
257
+fn spawn_garnotify() -> Option<std::process::Child> {
258
+    tracing::info!("Spawning garnotify...");
259
+
260
+    // Try to find garnotify in PATH or common locations
261
+    let garnotify_cmd = which_garnotify().unwrap_or_else(|| "garnotify".to_string());
262
+
263
+    // Inherit DISPLAY from current environment
264
+    let x_display = std::env::var("DISPLAY").unwrap_or_else(|_| ":0".to_string());
265
+
266
+    match Command::new(&garnotify_cmd)
267
+        .arg("daemon")
268
+        .env("DISPLAY", &x_display)
269
+        .spawn()
270
+    {
271
+        Ok(child) => {
272
+            tracing::info!("garnotify started (PID {}), DISPLAY={}", child.id(), x_display);
273
+
274
+            // Wait briefly and verify garnotify becomes healthy
275
+            for attempt in 1..=10 {
276
+                std::thread::sleep(std::time::Duration::from_millis(200));
277
+                if is_garnotify_healthy() {
278
+                    tracing::info!("garnotify socket ready after {}ms", attempt * 200);
279
+                    return Some(child);
280
+                }
281
+            }
282
+
283
+            // Socket never appeared - might still be starting up
284
+            tracing::warn!("garnotify socket not ready after 2s, may still be starting");
285
+            Some(child)
286
+        }
287
+        Err(e) => {
288
+            tracing::error!("Failed to spawn garnotify: {}", e);
289
+            tracing::info!("Hint: Ensure garnotify is installed and in PATH");
290
+            None
291
+        }
292
+    }
293
+}
294
+
295
+/// Find garnotify executable
296
+fn which_garnotify() -> Option<String> {
297
+    // Check if garnotify is in PATH
298
+    if Command::new("which")
299
+        .arg("garnotify")
300
+        .output()
301
+        .map(|o| o.status.success())
302
+        .unwrap_or(false)
303
+    {
304
+        return Some("garnotify".to_string());
305
+    }
306
+
307
+    // Check common cargo install location
308
+    if let Ok(home) = std::env::var("HOME") {
309
+        let cargo_bin = format!("{}/.cargo/bin/garnotify", home);
310
+        if std::path::Path::new(&cargo_bin).exists() {
311
+            return Some(cargo_bin);
312
+        }
313
+    }
314
+
315
+    // Check /usr/local/bin
316
+    if std::path::Path::new("/usr/local/bin/garnotify").exists() {
317
+        return Some("/usr/local/bin/garnotify".to_string());
318
+    }
319
+
320
+    None
321
+}
322
+
323
+/// Stop garnotify gracefully by sending SIGTERM.
324
+fn stop_garnotify(child: &mut std::process::Child) {
325
+    tracing::info!("Stopping garnotify (PID {})...", child.id());
326
+
327
+    // Send SIGTERM for graceful shutdown
328
+    unsafe {
329
+        libc::kill(child.id() as i32, libc::SIGTERM);
330
+    }
331
+
332
+    // Wait briefly for it to exit
333
+    match child.try_wait() {
334
+        Ok(Some(status)) => {
335
+            tracing::info!("garnotify exited with status: {}", status);
336
+        }
337
+        Ok(None) => {
338
+            std::thread::sleep(std::time::Duration::from_millis(100));
339
+            match child.try_wait() {
340
+                Ok(Some(status)) => {
341
+                    tracing::info!("garnotify exited with status: {}", status);
342
+                }
343
+                Ok(None) => {
344
+                    tracing::warn!("garnotify did not exit gracefully, sending SIGKILL");
345
+                    let _ = child.kill();
346
+                }
347
+                Err(e) => {
348
+                    tracing::warn!("Error waiting for garnotify: {}", e);
349
+                }
350
+            }
351
+        }
352
+        Err(e) => {
353
+            tracing::warn!("Error checking garnotify status: {}", e);
354
+        }
355
+    }
356
+}
357
+
239358
 use x11rb::protocol::xproto::{
240359
     ButtonPressEvent, ButtonReleaseEvent, ClientMessageEvent, ConfigureRequestEvent,
241360
     ConfigureWindowAux, ConnectionExt, DestroyNotifyEvent, EnterNotifyEvent, EventMask,
@@ -2503,6 +2622,11 @@ impl WindowManager {
25032622
             self.garbar_process = spawn_garbar();
25042623
         }
25052624
 
2625
+        // Spawn garnotify if gar.notification is configured
2626
+        if self.config.notification_enabled {
2627
+            self.garnotify_process = spawn_garnotify();
2628
+        }
2629
+
25062630
         while self.running {
25072631
             // Handle X11 events (non-blocking poll)
25082632
             while let Some(event) = self.conn.conn.poll_for_event()? {
@@ -2539,6 +2663,12 @@ impl WindowManager {
25392663
         }
25402664
         self.garbar_process = None;
25412665
 
2666
+        // Stop garnotify if it was spawned
2667
+        if let Some(ref mut child) = self.garnotify_process {
2668
+            stop_garnotify(child);
2669
+        }
2670
+        self.garnotify_process = None;
2671
+
25422672
         // Kill picom to prevent compositor effects from bleeding into the greeter
25432673
         tracing::info!("Killing picom...");
25442674
         let _ = std::process::Command::new("pkill")