gardesk/gardisplay / d8541fd

Browse files

wire up apply/revert layout with keyboard shortcuts

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
d8541fd0dc66308f382df8bbd2591ef743d82187
Parents
da60768
Tree
237d563

2 changed files

StatusFile+-
M gardisplay/src/app.rs 217 7
M gardisplay/src/main.rs 1 0
gardisplay/src/app.rsmodified
@@ -1,14 +1,16 @@
11
 //! Main application state and event loop.
22
 
33
 use anyhow::Result;
4
-use gartk_core::{InputEvent, Key, Rect, Size, Theme};
4
+use gartk_core::{Color, InputEvent, Key, Rect, Size, Theme};
55
 use gartk_render::{copy_surface_to_window, Renderer};
66
 use gartk_x11::{
7
-    detect_monitors, primary_monitor, Connection, EventLoop, EventLoopConfig, Window, WindowConfig,
7
+    detect_monitors, primary_monitor, Connection, EventLoop, EventLoopConfig, Monitor, Window,
8
+    WindowConfig,
89
 };
910
 use x11rb::protocol::xproto::ConnectionExt;
1011
 
11
-use crate::config::Config;
12
+use crate::config::{Config, MonitorConfig};
13
+use crate::randr::RandrManager;
1214
 use crate::ui::{EventResult, MonitorView};
1315
 
1416
 /// Window dimensions.
@@ -17,15 +19,19 @@ const WINDOW_HEIGHT: u32 = 600;
1719
 
1820
 /// Main application.
1921
 pub struct App {
20
-    #[allow(dead_code)] // Used in Sprint 3 for RandR operations
22
+    #[allow(dead_code)] // Connection kept alive for X11 resources
2123
     conn: Connection,
2224
     window: Window,
2325
     renderer: Renderer,
2426
     theme: Theme,
2527
     gc: u32,
26
-    #[allow(dead_code)] // Used in Sprint 3 for profile management
28
+    #[allow(dead_code)] // Used for profile management
2729
     config: Config,
2830
     monitor_view: MonitorView,
31
+    randr: Option<RandrManager>,
32
+    original_monitors: Vec<Monitor>,
33
+    demo_mode: bool,
34
+    status_message: Option<(String, std::time::Instant)>,
2935
 }
3036
 
3137
 impl App {
@@ -82,6 +88,19 @@ impl App {
8288
         let view_rect = Rect::new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT - 100); // Leave room for controls
8389
         let mut monitor_view = MonitorView::new(view_rect);
8490
 
91
+        // Create RandR manager (only in non-demo mode)
92
+        let randr = if demo {
93
+            None
94
+        } else {
95
+            match RandrManager::new(conn.clone()) {
96
+                Ok(r) => Some(r),
97
+                Err(e) => {
98
+                    tracing::warn!("failed to create RandR manager: {}", e);
99
+                    None
100
+                }
101
+            }
102
+        };
103
+
85104
         // Detect or create demo monitors
86105
         let monitors = if demo {
87106
             tracing::info!("demo mode: using fake monitors");
@@ -101,6 +120,9 @@ impl App {
101120
                 if m.primary { "(primary)" } else { "" }
102121
             );
103122
         }
123
+
124
+        // Store original state for reverting
125
+        let original_monitors = monitors.clone();
104126
         monitor_view.set_monitors(monitors);
105127
 
106128
         Ok(Self {
@@ -111,6 +133,10 @@ impl App {
111133
             gc,
112134
             config,
113135
             monitor_view,
136
+            randr,
137
+            original_monitors,
138
+            demo_mode: demo,
139
+            status_message: None,
114140
         })
115141
     }
116142
 
@@ -178,6 +204,24 @@ impl App {
178204
             InputEvent::CloseRequested => EventResult::Quit,
179205
             InputEvent::Key(e) if e.pressed && e.key == Key::Escape => EventResult::Quit,
180206
             InputEvent::Key(e) if e.pressed && e.key == Key::Char('q') => EventResult::Quit,
207
+            // Apply: Ctrl+A or Enter
208
+            InputEvent::Key(e)
209
+                if e.pressed
210
+                    && (e.key == Key::Return
211
+                        || (e.modifiers.ctrl && e.key == Key::Char('a'))) =>
212
+            {
213
+                self.apply_layout();
214
+                EventResult::Redraw
215
+            }
216
+            // Revert: Ctrl+R or Backspace
217
+            InputEvent::Key(e)
218
+                if e.pressed
219
+                    && (e.key == Key::Backspace
220
+                        || (e.modifiers.ctrl && e.key == Key::Char('r'))) =>
221
+            {
222
+                self.revert_layout();
223
+                EventResult::Redraw
224
+            }
181225
             InputEvent::Resize { width, height } => {
182226
                 self.handle_resize(Size::new(*width, *height));
183227
                 EventResult::Redraw
@@ -187,6 +231,140 @@ impl App {
187231
         }
188232
     }
189233
 
234
+    /// Apply the current layout via RandR.
235
+    fn apply_layout(&mut self) {
236
+        if self.demo_mode {
237
+            self.set_status("Demo mode - changes not applied");
238
+            return;
239
+        }
240
+
241
+        let Some(ref randr) = self.randr else {
242
+            self.set_status("RandR not available");
243
+            return;
244
+        };
245
+
246
+        // Build MonitorConfig from current view state
247
+        // Collect all data we need before releasing the borrow
248
+        let primary_name = self.monitor_view.primary_name().map(|s| s.to_string());
249
+        let configs: Vec<(MonitorConfig, Monitor)> = self
250
+            .monitor_view
251
+            .monitors()
252
+            .iter()
253
+            .map(|state| {
254
+                let config = MonitorConfig {
255
+                    name: state.info.name.clone(),
256
+                    enabled: true,
257
+                    x: state.real_position.x,
258
+                    y: state.real_position.y,
259
+                    width: state.info.rect.width,
260
+                    height: state.info.rect.height,
261
+                    refresh: 60.0, // TODO: get actual refresh rate
262
+                    scale: 1.0,
263
+                    rotation: 0,
264
+                };
265
+                let monitor = Monitor {
266
+                    name: state.info.name.clone(),
267
+                    rect: Rect::new(
268
+                        state.real_position.x,
269
+                        state.real_position.y,
270
+                        state.info.rect.width,
271
+                        state.info.rect.height,
272
+                    ),
273
+                    primary: primary_name.as_ref() == Some(&state.info.name),
274
+                    width_mm: state.info.width_mm,
275
+                    height_mm: state.info.height_mm,
276
+                };
277
+                (config, monitor)
278
+            })
279
+            .collect();
280
+
281
+        let mut success_count = 0;
282
+        let mut error_count = 0;
283
+
284
+        for (config, _) in &configs {
285
+            match randr.apply_monitor(config) {
286
+                Ok(()) => success_count += 1,
287
+                Err(e) => {
288
+                    tracing::error!("failed to apply config for {}: {}", config.name, e);
289
+                    error_count += 1;
290
+                }
291
+            }
292
+        }
293
+
294
+        // Set primary
295
+        if let Some(ref name) = primary_name {
296
+            if let Err(e) = randr.set_primary(name) {
297
+                tracing::error!("failed to set primary: {}", e);
298
+            }
299
+        }
300
+
301
+        if let Err(e) = randr.flush() {
302
+            tracing::error!("failed to flush: {}", e);
303
+        }
304
+
305
+        if error_count == 0 {
306
+            self.set_status(&format!("Applied {} monitor(s)", success_count));
307
+            // Update original state after successful apply
308
+            self.original_monitors = configs.into_iter().map(|(_, m)| m).collect();
309
+        } else {
310
+            self.set_status(&format!(
311
+                "Applied {} monitor(s), {} error(s)",
312
+                success_count, error_count
313
+            ));
314
+        }
315
+    }
316
+
317
+    /// Revert to the original layout.
318
+    fn revert_layout(&mut self) {
319
+        if self.demo_mode {
320
+            // In demo mode, just reset the view
321
+            self.monitor_view.set_monitors(self.original_monitors.clone());
322
+            self.set_status("Reverted to original layout");
323
+            return;
324
+        }
325
+
326
+        // Restore original monitors in view
327
+        self.monitor_view.set_monitors(self.original_monitors.clone());
328
+
329
+        // Apply via RandR
330
+        if let Some(ref randr) = self.randr {
331
+            for m in &self.original_monitors {
332
+                let config = MonitorConfig {
333
+                    name: m.name.clone(),
334
+                    enabled: true,
335
+                    x: m.rect.x,
336
+                    y: m.rect.y,
337
+                    width: m.rect.width,
338
+                    height: m.rect.height,
339
+                    refresh: 60.0,
340
+                    scale: 1.0,
341
+                    rotation: 0,
342
+                };
343
+
344
+                if let Err(e) = randr.apply_monitor(&config) {
345
+                    tracing::error!("failed to revert {}: {}", m.name, e);
346
+                }
347
+            }
348
+
349
+            // Restore primary
350
+            if let Some(m) = self.original_monitors.iter().find(|m| m.primary) {
351
+                if let Err(e) = randr.set_primary(&m.name) {
352
+                    tracing::error!("failed to restore primary: {}", e);
353
+                }
354
+            }
355
+
356
+            let _ = randr.flush();
357
+        }
358
+
359
+        self.set_status("Reverted to original layout");
360
+    }
361
+
362
+    /// Set a status message to display temporarily.
363
+    fn set_status(&mut self, message: &str) {
364
+        tracing::info!("{}", message);
365
+        self.status_message = Some((message.to_string(), std::time::Instant::now()));
366
+    }
367
+
190368
     /// Handle window resize.
191369
     fn handle_resize(&mut self, size: Size) {
192370
         tracing::debug!("resize to {}x{}", size.width, size.height);
@@ -227,14 +405,46 @@ impl App {
227405
             1.0,
228406
         )?;
229407
 
230
-        // Title in controls area
408
+        // Instructions
231409
         self.renderer.text_default(
232
-            "gardisplay - Drag monitors to arrange",
410
+            "Drag monitors to arrange | Double-click to set primary",
233411
             10.0,
234412
             (controls_y + 20) as f64,
235413
             self.theme.foreground,
236414
         )?;
237415
 
416
+        // Keyboard shortcuts
417
+        self.renderer.text_default(
418
+            "Enter: Apply | Backspace: Revert | Q/Esc: Quit",
419
+            10.0,
420
+            (controls_y + 40) as f64,
421
+            self.theme.item_description,
422
+        )?;
423
+
424
+        // Status message (show for 3 seconds)
425
+        if let Some((ref msg, instant)) = self.status_message {
426
+            if instant.elapsed().as_secs() < 3 {
427
+                // Green for success, theme color otherwise
428
+                let color = if msg.contains("error") {
429
+                    Color::new(1.0, 0.4, 0.4, 1.0) // Red
430
+                } else {
431
+                    Color::new(0.4, 0.8, 0.4, 1.0) // Green
432
+                };
433
+                self.renderer
434
+                    .text_default(msg, 10.0, (controls_y + 70) as f64, color)?;
435
+            }
436
+        }
437
+
438
+        // Dirty indicator
439
+        if self.monitor_view.is_dirty() {
440
+            self.renderer.text_default(
441
+                "(unsaved changes)",
442
+                (size.width - 150) as f64,
443
+                (controls_y + 20) as f64,
444
+                Color::new(1.0, 0.7, 0.3, 1.0), // Orange
445
+            )?;
446
+        }
447
+
238448
         // Blit to window
239449
         copy_surface_to_window(self.renderer.surface_mut(), &self.window, self.gc, 0, 0)?;
240450
 
gardisplay/src/main.rsmodified
@@ -2,6 +2,7 @@
22
 
33
 mod app;
44
 mod config;
5
+mod randr;
56
 mod ui;
67
 
78
 use clap::Parser;