@@ -14,6 +14,7 @@ mod background; |
| 14 | 14 | mod config; |
| 15 | 15 | mod error; |
| 16 | 16 | mod keyboard; |
| 17 | +mod overlay; |
| 17 | 18 | mod password; |
| 18 | 19 | mod ring; |
| 19 | 20 | mod screenshot; |
@@ -23,6 +24,7 @@ mod x11; |
| 23 | 24 | use auth::{authenticate_async, get_current_username, AuthResult, PendingAuth}; |
| 24 | 25 | use background::Background; |
| 25 | 26 | use config::Config; |
| 27 | +use overlay::{composite_overlay, OverlayRenderer}; |
| 26 | 28 | use keyboard::{KeyResult, Keyboard}; |
| 27 | 29 | use password::Password; |
| 28 | 30 | use ring::{composite_ring, RingRenderer}; |
@@ -153,6 +155,9 @@ fn run_lock(config: Config) -> Result<()> { |
| 153 | 155 | tracing::info!("Initializing ring indicator..."); |
| 154 | 156 | let mut ring = RingRenderer::from_config(&config.ring); |
| 155 | 157 | |
| 158 | + // Step 7: Initialize overlay renderer for text elements |
| 159 | + let overlay = OverlayRenderer::new(&config.font); |
| 160 | + |
| 156 | 161 | // Get ring center position (center of primary monitor, or screen center) |
| 157 | 162 | let (ring_cx, ring_cy) = match locker.get_monitors() { |
| 158 | 163 | Ok(monitors) => { |
@@ -175,13 +180,24 @@ fn run_lock(config: Config) -> Result<()> { |
| 175 | 180 | } |
| 176 | 181 | }; |
| 177 | 182 | |
| 178 | | - // Helper to composite ring onto background and display |
| 183 | + /// Overlay positioning offset from ring center |
| 184 | + const OVERLAY_TIME_OFFSET_Y: i32 = -150; // Above ring |
| 185 | + const OVERLAY_CAPS_OFFSET_Y: i32 = 130; // Below ring |
| 186 | + const OVERLAY_ATTEMPTS_OFFSET_Y: i32 = 160; // Below caps lock |
| 187 | + const OVERLAY_COOLDOWN_OFFSET_Y: i32 = 190; // Below attempts |
| 188 | + |
| 189 | + // Helper to composite ring and overlays onto background and display |
| 179 | 190 | let render_frame = |background: &mut Background, |
| 180 | 191 | background_clean: &[u8], |
| 181 | 192 | ring: &RingRenderer, |
| 193 | + overlay: &OverlayRenderer, |
| 182 | 194 | locker: &LockerWindow, |
| 195 | + config: &Config, |
| 183 | 196 | ring_cx: i32, |
| 184 | | - ring_cy: i32| |
| 197 | + ring_cy: i32, |
| 198 | + caps_lock_active: bool, |
| 199 | + failed_attempts: u32, |
| 200 | + cooldown_secs: u64| |
| 185 | 201 | -> Result<()> { |
| 186 | 202 | // Reset background to clean state |
| 187 | 203 | background.data.copy_from_slice(background_clean); |
@@ -206,6 +222,78 @@ fn run_lock(config: Config) -> Result<()> { |
| 206 | 222 | dest_y, |
| 207 | 223 | ); |
| 208 | 224 | |
| 225 | + // Render and composite time display (above ring) |
| 226 | + if let Some(result) = overlay.render_time(&config.indicator) { |
| 227 | + let mut time_surface = result?; |
| 228 | + let time_data = time_surface.to_bgra()?; |
| 229 | + let time_x = ring_cx - time_surface.width / 2; |
| 230 | + let time_y = ring_cy + OVERLAY_TIME_OFFSET_Y - time_surface.height / 2; |
| 231 | + composite_overlay( |
| 232 | + &mut background.data, |
| 233 | + background.width, |
| 234 | + background.height, |
| 235 | + &time_data, |
| 236 | + time_surface.width, |
| 237 | + time_surface.height, |
| 238 | + time_x, |
| 239 | + time_y, |
| 240 | + ); |
| 241 | + } |
| 242 | + |
| 243 | + // Render and composite caps lock indicator (below ring) |
| 244 | + if let Some(result) = overlay.render_caps_lock(&config.indicator, caps_lock_active) { |
| 245 | + let mut caps_surface = result?; |
| 246 | + let caps_data = caps_surface.to_bgra()?; |
| 247 | + let caps_x = ring_cx - caps_surface.width / 2; |
| 248 | + let caps_y = ring_cy + OVERLAY_CAPS_OFFSET_Y - caps_surface.height / 2; |
| 249 | + composite_overlay( |
| 250 | + &mut background.data, |
| 251 | + background.width, |
| 252 | + background.height, |
| 253 | + &caps_data, |
| 254 | + caps_surface.width, |
| 255 | + caps_surface.height, |
| 256 | + caps_x, |
| 257 | + caps_y, |
| 258 | + ); |
| 259 | + } |
| 260 | + |
| 261 | + // Render and composite failed attempts indicator |
| 262 | + if let Some(result) = overlay.render_failed_attempts(&config.indicator, failed_attempts) { |
| 263 | + let mut attempts_surface = result?; |
| 264 | + let attempts_data = attempts_surface.to_bgra()?; |
| 265 | + let attempts_x = ring_cx - attempts_surface.width / 2; |
| 266 | + let attempts_y = ring_cy + OVERLAY_ATTEMPTS_OFFSET_Y - attempts_surface.height / 2; |
| 267 | + composite_overlay( |
| 268 | + &mut background.data, |
| 269 | + background.width, |
| 270 | + background.height, |
| 271 | + &attempts_data, |
| 272 | + attempts_surface.width, |
| 273 | + attempts_surface.height, |
| 274 | + attempts_x, |
| 275 | + attempts_y, |
| 276 | + ); |
| 277 | + } |
| 278 | + |
| 279 | + // Render and composite cooldown timer |
| 280 | + if let Some(result) = overlay.render_cooldown(cooldown_secs) { |
| 281 | + let mut cooldown_surface = result?; |
| 282 | + let cooldown_data = cooldown_surface.to_bgra()?; |
| 283 | + let cooldown_x = ring_cx - cooldown_surface.width / 2; |
| 284 | + let cooldown_y = ring_cy + OVERLAY_COOLDOWN_OFFSET_Y - cooldown_surface.height / 2; |
| 285 | + composite_overlay( |
| 286 | + &mut background.data, |
| 287 | + background.width, |
| 288 | + background.height, |
| 289 | + &cooldown_data, |
| 290 | + cooldown_surface.width, |
| 291 | + cooldown_surface.height, |
| 292 | + cooldown_x, |
| 293 | + cooldown_y, |
| 294 | + ); |
| 295 | + } |
| 296 | + |
| 209 | 297 | locker.put_image(&background.data)?; |
| 210 | 298 | Ok(()) |
| 211 | 299 | }; |
@@ -215,9 +303,14 @@ fn run_lock(config: Config) -> Result<()> { |
| 215 | 303 | &mut background, |
| 216 | 304 | &background_clean, |
| 217 | 305 | &ring, |
| 306 | + &overlay, |
| 218 | 307 | &locker, |
| 308 | + &config, |
| 219 | 309 | ring_cx, |
| 220 | 310 | ring_cy, |
| 311 | + keyboard.caps_lock_active(), |
| 312 | + locker_state.failed_attempts, |
| 313 | + locker_state.cooldown_remaining(), |
| 221 | 314 | )?; |
| 222 | 315 | |
| 223 | 316 | // Get current username for PAM authentication |
@@ -227,10 +320,17 @@ fn run_lock(config: Config) -> Result<()> { |
| 227 | 320 | // Track pending authentication |
| 228 | 321 | let mut pending_auth: Option<PendingAuth> = None; |
| 229 | 322 | |
| 230 | | - tracing::info!("Entering event loop (press Escape to exit - dev mode only)"); |
| 323 | + #[cfg(feature = "dev")] |
| 324 | + tracing::info!("Entering event loop (dev mode: press Escape to exit)"); |
| 325 | + #[cfg(not(feature = "dev"))] |
| 326 | + tracing::info!("Entering event loop"); |
| 231 | 327 | |
| 232 | 328 | let mut needs_redraw = false; |
| 233 | 329 | |
| 330 | + // Timer for periodic time display updates (every second if time is shown) |
| 331 | + let mut last_time_update = std::time::Instant::now(); |
| 332 | + let time_update_interval = std::time::Duration::from_secs(1); |
| 333 | + |
| 234 | 334 | loop { |
| 235 | 335 | // Process events |
| 236 | 336 | match locker.poll_for_event()? { |
@@ -244,11 +344,17 @@ fn run_lock(config: Config) -> Result<()> { |
| 244 | 344 | let key_result = keyboard.process_key(keycode, true); |
| 245 | 345 | |
| 246 | 346 | match key_result { |
| 347 | + #[cfg(feature = "dev")] |
| 247 | 348 | KeyResult::Escape => { |
| 248 | 349 | tracing::info!("Escape pressed, exiting (dev mode)"); |
| 249 | 350 | break; |
| 250 | 351 | } |
| 251 | 352 | |
| 353 | + #[cfg(not(feature = "dev"))] |
| 354 | + KeyResult::Escape => { |
| 355 | + // In production, escape does nothing |
| 356 | + } |
| 357 | + |
| 252 | 358 | KeyResult::Char(c) => { |
| 253 | 359 | if password.push(c) { |
| 254 | 360 | locker_state.on_input(InputState::Letter); |
@@ -375,6 +481,12 @@ fn run_lock(config: Config) -> Result<()> { |
| 375 | 481 | needs_redraw = true; |
| 376 | 482 | } |
| 377 | 483 | |
| 484 | + // Periodic time display update (if enabled) |
| 485 | + if config.indicator.show_time && last_time_update.elapsed() >= time_update_interval { |
| 486 | + last_time_update = std::time::Instant::now(); |
| 487 | + needs_redraw = true; |
| 488 | + } |
| 489 | + |
| 378 | 490 | if needs_redraw { |
| 379 | 491 | // Update ring state from locker state |
| 380 | 492 | ring.set_state(locker_state.ring_state()); |
@@ -383,9 +495,14 @@ fn run_lock(config: Config) -> Result<()> { |
| 383 | 495 | &mut background, |
| 384 | 496 | &background_clean, |
| 385 | 497 | &ring, |
| 498 | + &overlay, |
| 386 | 499 | &locker, |
| 500 | + &config, |
| 387 | 501 | ring_cx, |
| 388 | 502 | ring_cy, |
| 503 | + keyboard.caps_lock_active(), |
| 504 | + locker_state.failed_attempts, |
| 505 | + locker_state.cooldown_remaining(), |
| 389 | 506 | )?; |
| 390 | 507 | needs_redraw = false; |
| 391 | 508 | } |