gardesk/garlock / f6727e8

Browse files

Add main entry point and event loop

- CLI with clap: --daemon, --config, --debug, --lock
- Screenshot capture and blur on startup
- Keyboard input handling with ring feedback
- PAM authentication on Enter key
- Timer-based state updates and rendering
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
f6727e8a7f2c193ff3dc42caf1500b95f779e133
Parents
05cfe0d
Tree
e52b16e

1 changed file

StatusFile+-
A garlock/src/main.rs 388 0
garlock/src/main.rsadded
@@ -0,0 +1,388 @@
1
+//! garlock - Screen locker for the gar desktop suite
2
+//!
3
+//! A swaylock-inspired screen locker with circular ring indicator,
4
+//! blur effects, and PAM authentication.
5
+
6
+use anyhow::Result;
7
+use clap::Parser;
8
+use std::path::PathBuf;
9
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
10
+use x11rb::connection::Connection;
11
+
12
+mod auth;
13
+mod background;
14
+mod config;
15
+mod error;
16
+mod keyboard;
17
+mod password;
18
+mod ring;
19
+mod screenshot;
20
+mod state;
21
+mod x11;
22
+
23
+use auth::{authenticate_async, get_current_username, AuthResult, PendingAuth};
24
+use background::Background;
25
+use config::Config;
26
+use keyboard::{KeyResult, Keyboard};
27
+use password::Password;
28
+use ring::{composite_ring, RingRenderer};
29
+use screenshot::Screenshot;
30
+use state::{InputState, LockerState};
31
+use x11::LockerWindow;
32
+
33
+/// Screen locker for the gar desktop suite
34
+#[derive(Parser, Debug)]
35
+#[command(name = "garlock", version, about, long_about = None)]
36
+struct Args {
37
+    /// Run in daemon mode (listen for IPC lock commands)
38
+    #[arg(long)]
39
+    daemon: bool,
40
+
41
+    /// Config file path (default: ~/.config/garlock/config.toml)
42
+    #[arg(long, short)]
43
+    config: Option<PathBuf>,
44
+
45
+    /// Enable debug logging
46
+    #[arg(long, short)]
47
+    debug: bool,
48
+
49
+    /// Lock immediately without daemon mode
50
+    #[arg(long)]
51
+    lock: bool,
52
+}
53
+
54
+fn main() -> Result<()> {
55
+    let args = Args::parse();
56
+
57
+    // Initialize logging
58
+    let log_level = if args.debug { "debug" } else { "info" };
59
+    tracing_subscriber::registry()
60
+        .with(
61
+            EnvFilter::try_from_default_env()
62
+                .unwrap_or_else(|_| EnvFilter::new(log_level)),
63
+        )
64
+        .with(tracing_subscriber::fmt::layer())
65
+        .init();
66
+
67
+    tracing::info!("garlock {} starting", env!("CARGO_PKG_VERSION"));
68
+    tracing::debug!(?args, "Command line arguments");
69
+
70
+    // Load configuration
71
+    let config = Config::load(args.config.as_deref())?;
72
+    tracing::debug!(?config, "Configuration loaded");
73
+
74
+    if args.daemon {
75
+        tracing::info!("Running in daemon mode");
76
+        run_daemon(config)
77
+    } else {
78
+        tracing::info!("Locking screen");
79
+        run_lock(config)
80
+    }
81
+}
82
+
83
+/// Run in daemon mode, listening for IPC lock commands
84
+fn run_daemon(_config: Config) -> Result<()> {
85
+    // TODO: Sprint 7 - IPC server implementation
86
+    tracing::warn!("Daemon mode not yet implemented");
87
+    Ok(())
88
+}
89
+
90
+/// Lock the screen immediately
91
+fn run_lock(config: Config) -> Result<()> {
92
+    // Step 1: Capture screenshot BEFORE creating locker window
93
+    tracing::info!("Capturing screenshot...");
94
+    let mut background = match Screenshot::capture() {
95
+        Ok(screenshot) => {
96
+            tracing::debug!(
97
+                width = screenshot.width,
98
+                height = screenshot.height,
99
+                "Screenshot captured, applying blur"
100
+            );
101
+
102
+            // Process with blur and brightness from config
103
+            match Background::from_screenshot(
104
+                screenshot,
105
+                config.background.blur_radius,
106
+                config.background.brightness,
107
+            ) {
108
+                Ok(bg) => bg,
109
+                Err(e) => {
110
+                    tracing::warn!("Failed to process screenshot: {}, using fallback", e);
111
+                    let (conn, screen_num) = x11rb::connect(None)?;
112
+                    let screen = &conn.setup().roots[screen_num];
113
+                    Background::solid_color(
114
+                        screen.width_in_pixels as u32,
115
+                        screen.height_in_pixels as u32,
116
+                        &config.background.fallback_color,
117
+                    )
118
+                }
119
+            }
120
+        }
121
+        Err(e) => {
122
+            tracing::warn!("Failed to capture screenshot: {}, using fallback", e);
123
+            let (conn, screen_num) = x11rb::connect(None)?;
124
+            let screen = &conn.setup().roots[screen_num];
125
+            Background::solid_color(
126
+                screen.width_in_pixels as u32,
127
+                screen.height_in_pixels as u32,
128
+                &config.background.fallback_color,
129
+            )
130
+        }
131
+    };
132
+
133
+    // Keep a clean copy of the background for re-compositing
134
+    let background_clean = background.data.clone();
135
+    let bg_width = background.width;
136
+    let bg_height = background.height;
137
+
138
+    // Step 2: Create fullscreen locker window with input grabs
139
+    tracing::info!("Creating locker window...");
140
+    let locker = LockerWindow::new()?;
141
+
142
+    // Step 3: Initialize keyboard handler with XKB
143
+    tracing::info!("Initializing keyboard handler...");
144
+    let mut keyboard = Keyboard::new()?;
145
+
146
+    // Step 4: Initialize password buffer
147
+    let mut password = Password::new();
148
+
149
+    // Step 5: Initialize state machine
150
+    let mut locker_state = LockerState::new();
151
+
152
+    // Step 6: Initialize ring renderer
153
+    tracing::info!("Initializing ring indicator...");
154
+    let mut ring = RingRenderer::from_config(&config.ring);
155
+
156
+    // Get ring center position (center of primary monitor, or screen center)
157
+    let (ring_cx, ring_cy) = match locker.get_monitors() {
158
+        Ok(monitors) => {
159
+            if let Some(primary) = monitors.primary() {
160
+                let (cx, cy) = primary.center();
161
+                tracing::info!(
162
+                    monitor = %primary.name,
163
+                    center_x = cx,
164
+                    center_y = cy,
165
+                    "Ring centered on primary monitor"
166
+                );
167
+                (cx as i32, cy as i32)
168
+            } else {
169
+                (bg_width as i32 / 2, bg_height as i32 / 2)
170
+            }
171
+        }
172
+        Err(e) => {
173
+            tracing::warn!("Failed to detect monitors: {}, centering on screen", e);
174
+            (bg_width as i32 / 2, bg_height as i32 / 2)
175
+        }
176
+    };
177
+
178
+    // Helper to composite ring onto background and display
179
+    let render_frame = |background: &mut Background,
180
+                        background_clean: &[u8],
181
+                        ring: &RingRenderer,
182
+                        locker: &LockerWindow,
183
+                        ring_cx: i32,
184
+                        ring_cy: i32|
185
+     -> Result<()> {
186
+        // Reset background to clean state
187
+        background.data.copy_from_slice(background_clean);
188
+
189
+        // Render ring to surface
190
+        let mut ring_surface = ring.render()?;
191
+        let ring_data = RingRenderer::surface_to_bgra(&mut ring_surface)?;
192
+        let (ring_w, ring_h) = ring.size();
193
+
194
+        // Composite ring onto background
195
+        let dest_x = ring_cx - ring_w / 2;
196
+        let dest_y = ring_cy - ring_h / 2;
197
+
198
+        composite_ring(
199
+            &mut background.data,
200
+            background.width,
201
+            background.height,
202
+            &ring_data,
203
+            ring_w as u32,
204
+            ring_h as u32,
205
+            dest_x,
206
+            dest_y,
207
+        );
208
+
209
+        locker.put_image(&background.data)?;
210
+        Ok(())
211
+    };
212
+
213
+    // Initial render
214
+    render_frame(
215
+        &mut background,
216
+        &background_clean,
217
+        &ring,
218
+        &locker,
219
+        ring_cx,
220
+        ring_cy,
221
+    )?;
222
+
223
+    // Get current username for PAM authentication
224
+    let username = get_current_username()?;
225
+    tracing::info!(%username, "Locking session for user");
226
+
227
+    // Track pending authentication
228
+    let mut pending_auth: Option<PendingAuth> = None;
229
+
230
+    tracing::info!("Entering event loop (press Escape to exit - dev mode only)");
231
+
232
+    let mut needs_redraw = false;
233
+
234
+    loop {
235
+        // Process events
236
+        match locker.poll_for_event()? {
237
+            Some(event) => {
238
+                match event {
239
+                    x11rb::protocol::Event::KeyPress(key_event) => {
240
+                        let keycode = key_event.detail;
241
+                        tracing::debug!(keycode, "Key press");
242
+
243
+                        // Process key through XKB
244
+                        let key_result = keyboard.process_key(keycode, true);
245
+
246
+                        match key_result {
247
+                            KeyResult::Escape => {
248
+                                tracing::info!("Escape pressed, exiting (dev mode)");
249
+                                break;
250
+                            }
251
+
252
+                            KeyResult::Char(c) => {
253
+                                if password.push(c) {
254
+                                    locker_state.on_input(InputState::Letter);
255
+                                    ring.set_state(locker_state.ring_state());
256
+                                    ring.advance_highlight();
257
+                                    needs_redraw = true;
258
+                                    tracing::debug!(
259
+                                        chars = password.char_count(),
260
+                                        "Character added to password"
261
+                                    );
262
+                                }
263
+                            }
264
+
265
+                            KeyResult::Backspace => {
266
+                                if password.pop() {
267
+                                    locker_state.on_input(InputState::Backspace);
268
+                                    ring.set_state(locker_state.ring_state());
269
+                                    ring.retreat_highlight();
270
+                                } else {
271
+                                    locker_state.on_input(InputState::Clear);
272
+                                    ring.set_state(locker_state.ring_state());
273
+                                    ring.clear_highlight();
274
+                                }
275
+                                needs_redraw = true;
276
+                                tracing::debug!(chars = password.char_count(), "Backspace");
277
+                            }
278
+
279
+                            KeyResult::Clear => {
280
+                                password.clear();
281
+                                locker_state.on_input(InputState::Clear);
282
+                                ring.set_state(locker_state.ring_state());
283
+                                ring.clear_highlight();
284
+                                needs_redraw = true;
285
+                                tracing::debug!("Password cleared (Ctrl+U)");
286
+                            }
287
+
288
+                            KeyResult::Enter => {
289
+                                if !password.is_empty() && pending_auth.is_none() {
290
+                                    locker_state.start_validation();
291
+                                    ring.set_state(locker_state.ring_state());
292
+                                    needs_redraw = true;
293
+                                    tracing::info!(
294
+                                        chars = password.char_count(),
295
+                                        "Starting PAM authentication"
296
+                                    );
297
+
298
+                                    // Start async PAM authentication
299
+                                    pending_auth = Some(authenticate_async(
300
+                                        username.clone(),
301
+                                        password.as_str().to_string(),
302
+                                    ));
303
+                                    password.clear();
304
+                                    ring.clear_highlight();
305
+                                }
306
+                            }
307
+
308
+                            KeyResult::Modifier => {
309
+                                locker_state.on_input(InputState::Neutral);
310
+                                // Check caps lock state
311
+                                if keyboard.caps_lock_active() {
312
+                                    tracing::debug!("Caps Lock is active");
313
+                                }
314
+                            }
315
+
316
+                            KeyResult::Ignored => {
317
+                                // Function keys, navigation, etc.
318
+                            }
319
+                        }
320
+                    }
321
+
322
+                    x11rb::protocol::Event::KeyRelease(key_event) => {
323
+                        // Update XKB state for key releases
324
+                        keyboard.process_key(key_event.detail, false);
325
+                    }
326
+
327
+                    x11rb::protocol::Event::Expose(_) => {
328
+                        tracing::trace!("Expose event");
329
+                        needs_redraw = true;
330
+                    }
331
+
332
+                    _ => {
333
+                        tracing::trace!(?event, "Unhandled event");
334
+                    }
335
+                }
336
+            }
337
+            None => {
338
+                // Check for pending authentication result
339
+                if let Some(ref auth) = pending_auth {
340
+                    if let Some(result) = auth.try_recv() {
341
+                        match result {
342
+                            AuthResult::Success => {
343
+                                tracing::info!("Authentication successful, unlocking screen");
344
+                                locker_state.on_auth_success();
345
+                                break; // Exit loop to unlock
346
+                            }
347
+                            AuthResult::Failure(msg) => {
348
+                                tracing::warn!("Authentication failed: {}", msg);
349
+                                locker_state.on_auth_failure();
350
+                                ring.set_state(locker_state.ring_state());
351
+                                needs_redraw = true;
352
+
353
+                                // TODO: Check for cooldown after max_attempts
354
+                            }
355
+                        }
356
+                        pending_auth = None;
357
+                    }
358
+                }
359
+
360
+                // Check timers and redraw if needed
361
+                if locker_state.update_timers() {
362
+                    needs_redraw = true;
363
+                }
364
+
365
+                if needs_redraw {
366
+                    // Update ring state from locker state
367
+                    ring.set_state(locker_state.ring_state());
368
+
369
+                    render_frame(
370
+                        &mut background,
371
+                        &background_clean,
372
+                        &ring,
373
+                        &locker,
374
+                        ring_cx,
375
+                        ring_cy,
376
+                    )?;
377
+                    needs_redraw = false;
378
+                }
379
+
380
+                // Sleep briefly to avoid busy loop
381
+                std::thread::sleep(std::time::Duration::from_millis(10));
382
+            }
383
+        }
384
+    }
385
+
386
+    tracing::info!("Screen locker exiting");
387
+    Ok(())
388
+}