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 @@
1
 //! Main application state and event loop.
1
 //! Main application state and event loop.
2
 
2
 
3
 use anyhow::Result;
3
 use anyhow::Result;
4
-use gartk_core::{InputEvent, Key, Rect, Size, Theme};
4
+use gartk_core::{Color, InputEvent, Key, Rect, Size, Theme};
5
 use gartk_render::{copy_surface_to_window, Renderer};
5
 use gartk_render::{copy_surface_to_window, Renderer};
6
 use gartk_x11::{
6
 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,
8
 };
9
 };
9
 use x11rb::protocol::xproto::ConnectionExt;
10
 use x11rb::protocol::xproto::ConnectionExt;
10
 
11
 
11
-use crate::config::Config;
12
+use crate::config::{Config, MonitorConfig};
13
+use crate::randr::RandrManager;
12
 use crate::ui::{EventResult, MonitorView};
14
 use crate::ui::{EventResult, MonitorView};
13
 
15
 
14
 /// Window dimensions.
16
 /// Window dimensions.
@@ -17,15 +19,19 @@ const WINDOW_HEIGHT: u32 = 600;
17
 
19
 
18
 /// Main application.
20
 /// Main application.
19
 pub struct App {
21
 pub struct App {
20
-    #[allow(dead_code)] // Used in Sprint 3 for RandR operations
22
+    #[allow(dead_code)] // Connection kept alive for X11 resources
21
     conn: Connection,
23
     conn: Connection,
22
     window: Window,
24
     window: Window,
23
     renderer: Renderer,
25
     renderer: Renderer,
24
     theme: Theme,
26
     theme: Theme,
25
     gc: u32,
27
     gc: u32,
26
-    #[allow(dead_code)] // Used in Sprint 3 for profile management
28
+    #[allow(dead_code)] // Used for profile management
27
     config: Config,
29
     config: Config,
28
     monitor_view: MonitorView,
30
     monitor_view: MonitorView,
31
+    randr: Option<RandrManager>,
32
+    original_monitors: Vec<Monitor>,
33
+    demo_mode: bool,
34
+    status_message: Option<(String, std::time::Instant)>,
29
 }
35
 }
30
 
36
 
31
 impl App {
37
 impl App {
@@ -82,6 +88,19 @@ impl App {
82
         let view_rect = Rect::new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT - 100); // Leave room for controls
88
         let view_rect = Rect::new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT - 100); // Leave room for controls
83
         let mut monitor_view = MonitorView::new(view_rect);
89
         let mut monitor_view = MonitorView::new(view_rect);
84
 
90
 
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
+
85
         // Detect or create demo monitors
104
         // Detect or create demo monitors
86
         let monitors = if demo {
105
         let monitors = if demo {
87
             tracing::info!("demo mode: using fake monitors");
106
             tracing::info!("demo mode: using fake monitors");
@@ -101,6 +120,9 @@ impl App {
101
                 if m.primary { "(primary)" } else { "" }
120
                 if m.primary { "(primary)" } else { "" }
102
             );
121
             );
103
         }
122
         }
123
+
124
+        // Store original state for reverting
125
+        let original_monitors = monitors.clone();
104
         monitor_view.set_monitors(monitors);
126
         monitor_view.set_monitors(monitors);
105
 
127
 
106
         Ok(Self {
128
         Ok(Self {
@@ -111,6 +133,10 @@ impl App {
111
             gc,
133
             gc,
112
             config,
134
             config,
113
             monitor_view,
135
             monitor_view,
136
+            randr,
137
+            original_monitors,
138
+            demo_mode: demo,
139
+            status_message: None,
114
         })
140
         })
115
     }
141
     }
116
 
142
 
@@ -178,6 +204,24 @@ impl App {
178
             InputEvent::CloseRequested => EventResult::Quit,
204
             InputEvent::CloseRequested => EventResult::Quit,
179
             InputEvent::Key(e) if e.pressed && e.key == Key::Escape => EventResult::Quit,
205
             InputEvent::Key(e) if e.pressed && e.key == Key::Escape => EventResult::Quit,
180
             InputEvent::Key(e) if e.pressed && e.key == Key::Char('q') => EventResult::Quit,
206
             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
+            }
181
             InputEvent::Resize { width, height } => {
225
             InputEvent::Resize { width, height } => {
182
                 self.handle_resize(Size::new(*width, *height));
226
                 self.handle_resize(Size::new(*width, *height));
183
                 EventResult::Redraw
227
                 EventResult::Redraw
@@ -187,6 +231,140 @@ impl App {
187
         }
231
         }
188
     }
232
     }
189
 
233
 
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
+
190
     /// Handle window resize.
368
     /// Handle window resize.
191
     fn handle_resize(&mut self, size: Size) {
369
     fn handle_resize(&mut self, size: Size) {
192
         tracing::debug!("resize to {}x{}", size.width, size.height);
370
         tracing::debug!("resize to {}x{}", size.width, size.height);
@@ -227,14 +405,46 @@ impl App {
227
             1.0,
405
             1.0,
228
         )?;
406
         )?;
229
 
407
 
230
-        // Title in controls area
408
+        // Instructions
231
         self.renderer.text_default(
409
         self.renderer.text_default(
232
-            "gardisplay - Drag monitors to arrange",
410
+            "Drag monitors to arrange | Double-click to set primary",
233
             10.0,
411
             10.0,
234
             (controls_y + 20) as f64,
412
             (controls_y + 20) as f64,
235
             self.theme.foreground,
413
             self.theme.foreground,
236
         )?;
414
         )?;
237
 
415
 
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
+
238
         // Blit to window
448
         // Blit to window
239
         copy_surface_to_window(self.renderer.surface_mut(), &self.window, self.gc, 0, 0)?;
449
         copy_surface_to_window(self.renderer.surface_mut(), &self.window, self.gc, 0, 0)?;
240
 
450
 
gardisplay/src/main.rsmodified
@@ -2,6 +2,7 @@
2
 
2
 
3
 mod app;
3
 mod app;
4
 mod config;
4
 mod config;
5
+mod randr;
5
 mod ui;
6
 mod ui;
6
 
7
 
7
 use clap::Parser;
8
 use clap::Parser;