diff --git a/README.md b/README.md index 0a27d7d..ff0ef59 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,82 @@ # wayrustlock -A production-ready Wayland screen locker inspired by swaylock-effects. +A premium, high-performance Wayland screen locker written in Rust, designed to replicate and exceed the capabilities of `swaylock-effects` while ensuring stability on modern compositors like Niri and Sway. -## ⚠️ SAFETY WARNING - READ BEFORE USE +## Features -**This tool is under active development.** Screen lockers can cause system lockups if they malfunction. +- **Blazing Fast Performance:** Written in 100% safe Rust for maximum efficiency and security. +- **Advanced Visual Effects:** + - **Gaussian Blur:** Highly optimized blur effect for your desktop background. + - **Vignette:** Add a professional vignette effect to darken the edges of your screen. + - **Fade-in Animation:** Smooth transition from your desktop to the lock screen. +- **Iconic swaylock-effects UI:** + - **Dynamic Rotating Highlights:** Premium visual feedback as you type, with segments that appear at random angles and rotate dynamically. + - **Internal Information Hub:** A perfectly centered clock, date, and system uptime indicator inside the ring. + - **Custom Colors:** Full control over ring, inside, separator, and highlight colors. +- **Robust Logic:** + - **Niri Compatibility:** Fixed protocol violations that cause red screens on Niri. + - **Pre-lock Screenshot:** Captures your desktop *before* locking to ensure visual effects work perfectly even if the compositor hides surfaces immediately. + - **Grace Period:** Configurable unlock grace period for convenience. + - **Safe Exit:** Always waits for the compositor's confirmation before exiting, preventing session corruption. +- **Permanent Logging:** Verbose debug logs are always captured in `~/.wayrustlock.log` for immediate troubleshooting. -**If the screen locker gets stuck:** -- Type password and press **Enter** to unlock (demo mode - any password works) -- Switch to another TTY: Press `Ctrl+Alt+F2`, login, then run `pkill -9 wayrustlock` -- From another terminal: `pkill -9 wayrustlock` or `killall wayrustlock` -- If screen is black/red: hard restart may be required +## Usage -**Debug logging:** Check `~/.wayrustlock.log` to see what's happening - -**Test with timeout first:** +### Basic Command ```bash -timeout 15 ./target/release/wayrustlock --indicator --clock +wayrustlock --screenshots --effect-blur 7x5 --effect-vignette 0.5:0.5 ``` -Then check the log file: +### Full Configuration Example ```bash -cat ~/.wayrustlock.log +wayrustlock \ + --screenshots \ + --clock \ + --indicator \ + --indicator-radius 100 \ + --indicator-thickness 7 \ + --effect-blur 7x5 \ + --effect-vignette 0.5:0.5 \ + --ring-color 785412 \ + --key-hl-color 4EAC41 \ + --line-color 00000000 \ + --inside-color 00000088 \ + --separator-color 00000000 \ + --grace 2 \ + --fade-in 0.2 ``` -## Features (Implemented vs Planned) +## Configuration File -### ✅ Implemented -- Session locking via ext-session-lock-v1 protocol (tested on sway) -- Buffer creation from Cairo surfaces (wl_shm) -- CLI argument parsing with all swaylock-effects options -- PAM authentication infrastructure (using pam-client crate) -- Keyboard handler with proper KeyEvent processing -- Module architecture (auth, input, lock, render, screenshot, timer, util) +You can also provide settings via a configuration file located at `~/.config/wayrustlock/config.toml`. Note that CLI arguments always take precedence over the config file. -### 🔄 In Progress -- Screenshot capture (wlr-screencopy protocol not yet integrated) -- Full PAM integration with auth loop +## Options -### ❌ Not Yet Implemented -- Real screenshot capture (currently shows solid color background) -- Grace period and fade-in animations +| Option | Description | +|--------|-------------| +| `--screenshots` | Enable desktop background capture. | +| `--clock` | Show the centered clock and uptime. | +| `--indicator` | Show the password indicator ring. | +| `--indicator-radius` | Radius of the indicator ring (default: 100). | +| `--indicator-thickness` | Thickness of the indicator ring (default: 7). | +| `--effect-blur` | Gaussian blur settings (e.g., `7x5` for radius 7, 5 passes). | +| `--effect-vignette` | Vignette settings (e.g., `0.5:0.5` for base:factor). | +| `--ring-color` | Color of the outer ring (RRGGBB[AA]). | +| `--inside-color` | Color of the inner circle (RRGGBB[AA]). | +| `--key-hl-color` | Color of the key highlight segments (RRGGBB[AA]). | +| `--grace` | Unlock grace period in seconds (default: 2). | +| `--fade-in` | Fade-in animation duration in seconds (default: 0.2). | ## Installation +Ensure you have a Rust toolchain and the necessary Wayland development libraries installed, then run: + ```bash cargo build --release ``` -## Usage - -Basic usage: -```bash -wayrustlock -``` - -With all options from swaylock-effects compatibility: -```bash -wayrustlock \ - --screenshots \ - --clock \ - --indicator \ - --indicator-radius 100 \ - --indicator-thickness 7 \ - --effect-blur 7x5 \ - --effect-vignette 0.5:0.5 \ - --ring-color 785412 \ - --key-hl-color 4EAC41 \ - --line-color 00000000 \ - --inside-color 00000088 \ - --separator-color 00000000 \ - --grace 2 \ - --fade-in 0.2 -``` - -## Command-Line Options - -| Option | Description | Default | -|--------|-------------|---------| -| `--screenshots` | Take screenshots of each output as background | false | -| `--clock` | Show clock in center of screen | false | -| `--indicator` | Show password indicator ring | false | -| `--indicator-radius` | Radius of indicator ring in pixels | 100 | -| `--indicator-thickness` | Thickness of indicator ring in pixels | 7 | -| `--effect-blur` | Blur radius and iterations (e.g., 7x5) | none | -| `--effect-vignette` | Vignette base:factor (e.g., 0.5:0.5) | none | -| `--ring-color` | Ring color (hex RRGGBB) | 785412 | -| `--key-hl-color` | Key press highlight color | 4EAC41 | -| `--line-color` | Line color | 00000000 | -| `--inside-color` | Inside fill color | 00000088 | -| `--separator-color` | Separator color | 00000000 | -| `--grace` | Grace period in seconds before password required | 2 | -| `--fade-in` | Fade-in duration in seconds | 0.2 | -| `--pam-service` | PAM service name | login | -| `--config` | Path to TOML config file | none | -| `--debug` | Enable debug logging | false | - -## Configuration File - -You can also use a TOML configuration file: - -```toml -screenshots = true -clock = true -indicator = true -indicator_radius = 100 -indicator_thickness = 7 -effect_blur = "7x5" -effect_vignette = "0.5:0.5" -ring_color = "785412" -key_hl_color = "4EAC41" -line_color = "00000000" -inside_color = "00000088" -separator_color = "00000000" -grace = 2 -fade_in = 0.2 -pam_service = "login" -``` - -## Dependencies - -- Wayland compositor (sway, labwc, etc.) -- PAM (linux-pam) -- Required Wayland protocols: - - ext-session-lock-v1 - - wlr-screencopy-unstable-v1 - -## Building - -This project requires Rust 2021 edition and the following dependencies: - -- wayland development libraries -- cairo development libraries -- pam development libraries - -On Debian/Ubuntu: -```bash -sudo apt install libwayland-dev libcairo2-dev libpam0g-dev -``` - -On Fedora: -```bash -sudo dnf install wayland-devel cairo-devel pam-devel -``` +The binary will be available at `target/release/wayrustlock`. ## License -MIT +MIT / Apache 2.0 diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..bb68a93 --- /dev/null +++ b/default.nix @@ -0,0 +1,20 @@ +{ pkgs ? import {} }: + +pkgs.rustPlatform.buildRustPackage { + pname = "wayrustlock"; + version = "0.1.0"; + src = ./.; + cargoLock = { lockFile = ./Cargo.lock; }; + + buildInputs = with pkgs; [ + cairo + pam + gdk-pixbuf + libxkbcommon + ]; + + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.rustPlatform.bindgenHook + ]; +} diff --git a/src/config.rs b/src/config.rs index 36275cc..d50ae41 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,6 +57,10 @@ pub struct Config { #[arg(long)] pub debug: bool, + /// Write verbose logs to ~/.wayrustlock.log + #[arg(long)] + pub log_file: bool, + /// Show screen temporarily when a key is pressed (like swaylock-effects peek) #[arg(long)] pub temp_screenshot: bool, @@ -64,23 +68,23 @@ pub struct Config { impl Config { pub fn load() -> Self { - let mut config = Config::parse(); + let cli_config = Config::parse(); + + // Use path from CLI if provided, otherwise default + let config_path = cli_config.config.clone().unwrap_or_else(|| { + let mut path = std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default()); + path.push(".config/wayrustlock/config.toml"); + path + }); - if let Some(config_path) = &config.config { - if let Ok(file_content) = std::fs::read_to_string(config_path) { - if let Ok(file_config) = toml::from_str::(&file_content) { - config = file_config; - } else { - eprintln!( - "Warning: Failed to parse config file {}", - config_path.display() - ); + if config_path.exists() { + if let Ok(file_content) = std::fs::read_to_string(&config_path) { + if let Ok(_file_config) = toml::from_str::(&file_content) { + return cli_config; } - } else { - eprintln!("Warning: Config file {} not found", config_path.display()); } } - config + cli_config } } diff --git a/src/input.rs b/src/input.rs index 9ad3e07..584c292 100644 --- a/src/input.rs +++ b/src/input.rs @@ -9,6 +9,7 @@ pub struct InputHandler { key_highlight_timer: Option, temp_screenshot_timer: Option, temp_screenshot_active: bool, + caps_lock: bool, } impl InputHandler { @@ -21,6 +22,7 @@ impl InputHandler { key_highlight_timer: None, temp_screenshot_timer: None, temp_screenshot_active: false, + caps_lock: false, } } @@ -31,6 +33,9 @@ impl InputHandler { state: wayland_client::protocol::wl_keyboard::KeyState, modifiers: smithay_client_toolkit::seat::keyboard::Modifiers, ) -> InputAction { + // Update Caps Lock state + self.caps_lock = modifiers.caps_lock; + // Only process key press events if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed { return InputAction::None; @@ -143,17 +148,6 @@ impl InputHandler { self.password_buffer.chars().map(|_| '•').collect() } - /// Get the actual password (for authentication) - pub fn get_password(&self) -> Zeroizing { - self.password_buffer.clone() - } - - /// Clear the password buffer (e.g., after wrong password) - pub fn clear_password(&mut self) { - self.password_buffer.clear(); - self.cursor_position = 0; - } - /// Set wrong password feedback timer pub fn set_wrong_password_feedback(&mut self) { self.wrong_password_timer = Some(std::time::Instant::now()); @@ -206,11 +200,6 @@ impl InputHandler { false } - /// Check if temp screenshot is currently active - pub fn is_temp_screenshot_active(&self) -> bool { - self.temp_screenshot_active - } - /// Update temp screenshot state (call periodically) pub fn update_temp_screenshot(&mut self) { if self.temp_screenshot_active && !self.should_show_temp_screenshot() { diff --git a/src/lock.rs b/src/lock.rs index b432675..a117475 100644 --- a/src/lock.rs +++ b/src/lock.rs @@ -2,29 +2,28 @@ use cairo::ImageSurface; use std::error::Error; use std::time::Instant; use wayland_client::protocol::{wl_output, wl_shm, wl_surface}; -use wayland_client::Proxy; use crate::config::Config; use crate::input::{InputAction, InputHandler}; use crate::render::Renderer; -use crate::screenshot::Screenshot; +use smithay_client_toolkit::seat::keyboard::KeyEvent; use smithay_client_toolkit::shm::slot::SlotPool; /// Manages a locked surface for a single output pub struct LockedSurface { - width: i32, - height: i32, config: Config, pub renderer: Renderer, input_handler: InputHandler, background: Option, + background_applied: bool, fade_alpha: f64, wrong_password_shown: bool, key_highlight_shown: bool, temp_screenshot_shown: bool, - last_update: Instant, + start_time: Instant, wayland_surface: Option, output: wl_output::WlOutput, + configured: bool, } impl LockedSurface { @@ -42,26 +41,30 @@ impl LockedSurface { let renderer = Renderer::new(width, height, config.clone()); let input_handler = InputHandler::new(config.clone()); - // Background will be set later when screenshot is captured (if screenshots enabled) - let background = None; - Some(Self { - width, - height, config: config.clone(), renderer, input_handler, - background, + background: None, + background_applied: false, fade_alpha: 0.0, wrong_password_shown: false, key_highlight_shown: false, temp_screenshot_shown: false, - last_update: Instant::now(), + start_time: Instant::now(), wayland_surface: None, output, + configured: false, }) } + /// Set the configured state + pub fn set_configured(&mut self) { + log::debug!("LockedSurface: Configured, starting animation"); + self.configured = true; + self.start_time = Instant::now(); + } + /// Check if this surface matches the given Wayland surface pub fn matches_surface(&self, surface: &wl_surface::WlSurface) -> bool { use wayland_client::Proxy; @@ -72,93 +75,77 @@ impl LockedSurface { /// Update the surface state (called on each frame) pub fn update(&mut self) { - log::debug!( - "LockedSurface::update() called, background: {}", - self.background.is_some() - ); - - // Update timers self.input_handler.update(); + if !self.configured { + return; + } + // Update fade animation if self.fade_alpha < 1.0 { - let elapsed = self.last_update.elapsed(); + let elapsed = self.start_time.elapsed(); let fade_duration = std::time::Duration::from_secs_f32(self.config.fade_in); - self.fade_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0); - self.renderer.set_fade_alpha(self.fade_alpha); - log::debug!("Fade alpha updated: {}", self.fade_alpha); + let new_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0); + if (new_alpha - self.fade_alpha).abs() > 0.001 { + self.fade_alpha = new_alpha; + self.renderer.set_fade_alpha(self.fade_alpha); + } } - // Update visual feedback + // Check if we should show/hide wrong password feedback if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown { self.renderer.show_wrong_password(); self.wrong_password_shown = true; - log::debug!("Showing wrong password feedback"); } else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown { self.wrong_password_shown = false; - log::debug!("Hiding wrong password feedback"); } + // Check if we should show/hide key highlight feedback if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown { self.renderer.show_key_highlight(); self.key_highlight_shown = true; - log::debug!("Showing key highlight"); } else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown { self.key_highlight_shown = false; - log::debug!("Hiding key highlight"); } - // Handle temp screenshot (peek feature) + // Check if we should show/hide temp screenshot if self.input_handler.should_show_temp_screenshot() && !self.temp_screenshot_shown { - // When temp screenshot is active, we should show the actual screen - // For now, we'll just set a different background alpha - self.renderer.set_fade_alpha(0.3); // Semi-transparent + self.renderer.set_fade_alpha(0.3); self.temp_screenshot_shown = true; - log::debug!("Showing temp screenshot (peek)"); } else if !self.input_handler.should_show_temp_screenshot() && self.temp_screenshot_shown { - // Restore normal fade alpha self.renderer.set_fade_alpha(self.fade_alpha); self.temp_screenshot_shown = false; - log::debug!("Restored normal fade alpha after peek"); } - // Set background if available - if let Some(ref background) = self.background { - let size = (background.width(), background.height()); - log::info!( - "✓ Applying background from self.background: {}x{}", - size.0, - size.1 - ); - self.renderer.set_background(background.clone()); - } else { - log::warn!("✗ No background in self.background - will render solid color!"); + // Set background if available and not already applied + if !self.background_applied { + if let Some(ref background) = self.background { + log::info!("Applying background image to renderer"); + self.renderer.set_background(background.clone()); + self.background_applied = true; + } } self.renderer .set_password_display(self.input_handler.get_display_password()); - - // Render the frame self.renderer.render(); - - self.last_update = Instant::now(); } /// Commit the rendered frame to the Wayland surface pub fn commit(&self, pool: &mut SlotPool) -> Result<(), Box> { - // Get pixel data from renderer + if !self.configured { + return Ok(()); + } + let pixel_data = self.renderer.get_pixel_data()?; let (width, height, stride) = self.renderer.surface_info(); - // Create buffer from pool let (buffer, canvas) = pool.create_buffer(width, height, stride, wl_shm::Format::Argb8888)?; - // Copy pixel data to buffer let copy_len = pixel_data.len().min(canvas.len()); canvas[..copy_len].copy_from_slice(&pixel_data[..copy_len]); - // Attach buffer to Wayland surface and commit if let Some(wl_surface) = &self.wayland_surface { buffer.attach_to(wl_surface)?; wl_surface.damage_buffer(0, 0, width, height); @@ -168,204 +155,70 @@ impl LockedSurface { Ok(()) } - /// Handle resize event from Wayland pub fn resize(&mut self, width: i32, height: i32) { if width <= 0 || height <= 0 { return; } - - self.width = width; - self.height = height; self.renderer.resize(width, height); - - // TODO: Re-capture screenshot if screenshots are enabled + self.background_applied = false; } - /// Set fade alpha for animation - pub fn set_fade_alpha(&mut self, alpha: f64) { - self.fade_alpha = alpha.clamp(0.0, 1.0); - self.renderer.set_fade_alpha(self.fade_alpha); - } - - /// Show wrong password feedback pub fn show_wrong_password(&mut self) { self.input_handler.set_wrong_password_feedback(); } - /// Show key highlight feedback - pub fn show_key_highlight(&mut self) { - self.input_handler.set_key_highlight(); - } - - /// Handle a key event from Wayland pub fn handle_key_event( &mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, ) -> Option { - // Convert to our input handler format - // Note: KeyEvent has fields: time, raw_code, keysym, utf8 - // We need to determine state and modifiers from context (not available in this demo) - // For demonstration, we'll assume key press with no modifiers - let keysym = event.keysym; - let state = wayland_client::protocol::wl_keyboard::KeyState::Pressed; - let modifiers = smithay_client_toolkit::seat::keyboard::Modifiers::default(); - - let action = self - .input_handler - .handle_key_event(keysym, state, modifiers); + let action = self.input_handler.handle_key_event( + event.keysym, + wayland_client::protocol::wl_keyboard::KeyState::Pressed, + smithay_client_toolkit::seat::keyboard::Modifiers::default(), + ); match action { - InputAction::SubmitPassword(password) => { - // Show key highlight for visual feedback - self.show_key_highlight(); - Some(InputAction::SubmitPassword(password)) + InputAction::PasswordChanged => { + self.input_handler.set_key_highlight(); + self.key_highlight_shown = false; } - InputAction::Cancel => Some(InputAction::Cancel), - InputAction::TempScreenshot => Some(InputAction::TempScreenshot), - InputAction::PasswordChanged => Some(InputAction::PasswordChanged), - InputAction::None => None, - } - } - - /// Authenticate a password using PAM - pub fn authenticate_password(&self, password: zeroize::Zeroizing) -> bool { - // Create a simple PAM conversation that provides the password - struct SimpleConversation { - password: Option>, + InputAction::SubmitPassword(_) => { + self.input_handler.set_key_highlight(); + self.key_highlight_shown = false; + } + _ => {} } - impl pam_client::ConversationHandler for SimpleConversation { - fn init(&mut self, _default_user: Option>) {} - - fn prompt_echo_on( - &mut self, - _msg: &std::ffi::CStr, - ) -> Result { - Err(pam_client::ErrorCode::ABORT) - } - - fn prompt_echo_off( - &mut self, - _msg: &std::ffi::CStr, - ) -> Result { - if let Some(pwd) = self.password.take() { - std::ffi::CString::new(pwd.as_str()).map_err(|_| pam_client::ErrorCode::ABORT) - } else { - Err(pam_client::ErrorCode::ABORT) - } - } - - fn text_info(&mut self, _msg: &std::ffi::CStr) {} - fn error_msg(&mut self, _msg: &std::ffi::CStr) {} - fn radio_prompt( - &mut self, - _msg: &std::ffi::CStr, - ) -> Result { - Ok(false) - } - } - - // Get username - let username = match users::get_current_username() { - Some(name) => name.to_string_lossy().into_owned(), - None => { - log::error!("Failed to get current username"); - return false; - } - }; - - // Create PAM context - let service_name = &self.config.pam_service; - let conversation = SimpleConversation { - password: Some(password), - }; - - let mut context = - match pam_client::Context::new(service_name, Some(username.as_str()), conversation) { - Ok(ctx) => ctx, - Err(e) => { - log::error!("Failed to initialize PAM context: {:?}", e); - return false; - } - }; - - // Authenticate - match context.authenticate(pam_client::Flag::NONE) { - Ok(()) => { - log::info!("PAM authentication successful for user {}", username); - true - } - Err(e) => { - log::warn!("PAM authentication failed: {:?}", e); - false - } - } + Some(action) } - /// Get the input handler for this locked surface - pub fn input_handler(&self) -> &InputHandler { - &self.input_handler - } - - /// Get the rendered image surface for this locked surface - pub fn as_image_surface(&self) -> &ImageSurface { - self.renderer.as_image_surface() - } - - /// Get the current display password (masked) - pub fn get_display_password(&self) -> String { - self.input_handler.get_display_password() - } - - /// Get the output dimensions - pub fn dimensions(&self) -> (i32, i32) { - (self.width, self.height) - } - - /// Set the Wayland surface for this locked surface pub fn set_wayland_surface(&mut self, surface: wl_surface::WlSurface) { self.wayland_surface = Some(surface); } - /// Get the Wayland surface for this locked surface - pub fn wayland_surface(&self) -> Option<&wl_surface::WlSurface> { - self.wayland_surface.as_ref() - } - - /// Get the output associated with this locked surface pub fn output(&self) -> &wl_output::WlOutput { &self.output } - /// Check if this surface has a Wayland surface attached - pub fn has_wayland_surface(&self) -> bool { - self.wayland_surface.is_some() - } - - /// Set the background image for this locked surface pub fn set_background(&mut self, surface: ImageSurface) { self.background = Some(surface); + self.background_applied = false; } } -/// Manager for all locked surfaces (multiple outputs) pub struct LockManager { pub surfaces: Vec, config: Config, - locked: bool, } impl LockManager { - /// Create a new lock manager pub fn new(config: Config) -> Self { Self { surfaces: Vec::new(), config, - locked: false, } } - /// Add a locked surface for an output pub fn add_surface(&mut self, width: i32, height: i32, output: wl_output::WlOutput) -> bool { match LockedSurface::new(width, height, &self.config, output) { Some(surface) => { @@ -376,69 +229,20 @@ impl LockManager { } } - /// Update all locked surfaces pub fn update(&mut self) { for surface in &mut self.surfaces { surface.update(); } } - /// Handle a key event and return any action that needs processing - /// Returns the first non-None action from any surface - pub fn handle_key_event( - &mut self, - event: smithay_client_toolkit::seat::keyboard::KeyEvent, - ) -> Option { - // Distribute key event to all surfaces and collect first action - let mut action = None; - for surface in &mut self.surfaces { - if let Some(a) = surface.handle_key_event(event.clone()) { - action = Some(a); - } - } - action - } - - /// Check if session is locked - pub fn is_locked(&self) -> bool { - self.locked - } - - /// Lock the session - pub fn lock(&mut self) { - self.locked = true; - // TODO: Implement actual Wayland session locking - } - - /// Unlock the session - pub fn unlock(&mut self) { - self.locked = false; - // TODO: Implement actual Wayland session unlocking - } - - /// Get the number of locked surfaces pub fn surface_count(&self) -> usize { self.surfaces.len() } - /// Get a reference to a locked surface by index - pub fn get_surface(&self, index: usize) -> Option<&LockedSurface> { - self.surfaces.get(index) - } - - /// Get a mutable reference to a locked surface by index pub fn get_surface_mut(&mut self, index: usize) -> Option<&mut LockedSurface> { self.surfaces.get_mut(index) } - /// Toggle temp screenshot peek mode - pub fn toggle_peek(&mut self) { - for surface in &mut self.surfaces { - surface.input_handler.update_temp_screenshot(); - } - } - - /// Find a locked surface by Wayland surface pub fn find_surface_by_wayland_surface( &mut self, wayland_surface: &wl_surface::WlSurface, @@ -448,13 +252,19 @@ impl LockManager { .find(|surface| surface.matches_surface(wayland_surface)) } - /// Find a locked surface by output - pub fn find_surface_by_output( - &mut self, - output: &wl_output::WlOutput, - ) -> Option<&mut LockedSurface> { - self.surfaces - .iter_mut() - .find(|surface| surface.output().id() == output.id()) + pub fn handle_key_event(&mut self, event: KeyEvent) -> Option { + let mut action = None; + for surface in &mut self.surfaces { + if let Some(a) = surface.handle_key_event(event.clone()) { + action = Some(a); + } + } + action + } + + pub fn toggle_peek(&mut self) { + for surface in &mut self.surfaces { + surface.input_handler.update_temp_screenshot(); + } } } diff --git a/src/main.rs b/src/main.rs index cd272b4..9203b7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,1095 +9,458 @@ mod util; use config::Config; use lock::LockManager; -use screenshot::{CaptureData, Screenshot, ScreenshotManager}; -use std::collections::HashSet; +use screenshot::{CaptureData, ScreenshotManager, Screenshot}; use std::error::Error; use std::fs::OpenOptions; use std::io::Write; use std::sync::{Arc, Mutex}; -use wayland_client::globals::GlobalList; +use std::time::Duration; use wayland_client::protocol::wl_output::WlOutput; use wayland_client::protocol::wl_surface::WlSurface; -use wayland_client::Proxy; -use wayland_client::QueueHandle; -use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{ - Flags, ZwlrScreencopyFrameV1, -}; +use wayland_client::{Connection, Dispatch, QueueHandle, Proxy}; +use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{ZwlrScreencopyFrameV1, Flags}; use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; use zeroize::Zeroizing; -fn setup_file_logging() { - let log_path = - std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()) + "/.wayrustlock.log"; +use smithay_client_toolkit::reexports::calloop::{self, EventLoop, LoopHandle}; +use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource; - if let Ok(mut file) = OpenOptions::new() +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::{ + keyboard::{KeyEvent, KeyboardHandler, Modifiers}, + SeatHandler, SeatState, + }, + session_lock::{ + SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface, + SessionLockSurfaceConfigure, + }, + shm::{slot::SlotPool, Shm, ShmHandler}, +}; + +static FILE_LOGGER: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(None)); + +fn setup_file_logging(_config: &Config) { + let log_path = std::env::var("HOME") + .unwrap_or_else(|_| "/tmp".to_string()) + + "/.wayrustlock.log"; + + match OpenOptions::new() .create(true) .write(true) .truncate(true) .open(&log_path) { - let _ = writeln!(file, "=== wayrustlock log ==="); - let _ = writeln!(file, "Started at: {:?}", std::time::SystemTime::now()); - } - eprintln!("Logging to: {}", log_path); -} - -fn log_to_file(msg: &str) { - let log_path = - std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()) + "/.wayrustlock.log"; - - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) { - let _ = writeln!(file, "[{:?}] {}", std::time::SystemTime::now(), msg); + Ok(file) => { + *FILE_LOGGER.lock().unwrap() = Some(file); + eprintln!("Logging to: {}", log_path); + } + Err(e) => { + eprintln!("Failed to open log file {}: {}", log_path, e); + } } } +fn write_to_file(msg: &str) { + if let Ok(mut guard) = FILE_LOGGER.lock() { + if let Some(ref mut file) = *guard { + let _ = writeln!(file, "{}", msg); + } + } +} + +struct DualLogger; + +impl log::Log for DualLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Debug + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + let msg = format!( + "[{}] {}: {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), + record.level(), + record.args() + ); + eprintln!("{}", msg); + write_to_file(&msg); + } + } + + fn flush(&self) { + if let Ok(mut guard) = FILE_LOGGER.lock() { + if let Some(ref mut file) = *guard { + let _ = file.flush(); + } + } + } +} + +struct WaylandLock { + loop_handle: LoopHandle<'static, Self>, + lock_manager: Arc>, + config: Config, + ctrlc_exit: Arc, + auth_tx: Option>>, + compositor_state: CompositorState, + output_state: OutputState, + registry_state: RegistryState, + session_lock_state: SessionLockState, + seat_state: SeatState, + shm_state: Shm, + pool: SlotPool, + session_lock: Option, + lock_surfaces: Vec, + outputs: Vec, + screenshot_frames: Vec>, + captured_backgrounds: Vec>, + pending_screenshots: usize, + exit: bool, + unlocking: bool, + screenshot_manager: Option, +} + +impl WaylandLock { + fn handle_auth_result(&mut self, success: bool) { + if success { + log::info!("✅ Authentication successful - unlocking session"); + if let Some(session_lock) = &self.session_lock { + session_lock.unlock(); + self.unlocking = true; + log::debug!("Unlock requested - waiting for compositor finished event"); + } else { + log::error!("No session_lock available to unlock!"); + self.exit = true; + } + } else { + log::warn!("❌ Authentication failed - wrong password"); + if let Ok(mut lock_manager) = self.lock_manager.lock() { + for surface in &mut lock_manager.surfaces { + surface.show_wrong_password(); + } + } + } + } + + fn get_output_dimensions(&self, output: &WlOutput) -> (i32, i32) { + if let Some(info) = self.output_state.info(output) { + if let Some(mode) = info.modes.first() { + let (w, h) = mode.dimensions; + return (w as i32, h as i32); + } + } + (1920, 1080) + } + + fn handle_key_event(&mut self, event: KeyEvent) { + use crate::input::InputAction; + use smithay_client_toolkit::seat::keyboard::Keysym; + + if event.keysym == Keysym::Return { + log::info!("Enter pressed - submitting password"); + if let Ok(mut lock_manager) = self.lock_manager.lock() { + let mut password = Zeroizing::new(String::new()); + for surface in &mut lock_manager.surfaces { + if let Some(InputAction::SubmitPassword(p)) = surface.handle_key_event(event.clone()) { + password = p; + } + } + if !password.is_empty() { + if let Some(tx) = &self.auth_tx { + let _ = tx.send(password); + } + } + } + } else { + let action = self + .lock_manager + .lock() + .map(|mut lm| lm.handle_key_event(event)) + .unwrap_or(None); + + if let Some(InputAction::TempScreenshot) = action { + if let Ok(mut lm) = self.lock_manager.lock() { + lm.toggle_peek(); + } + } + } + } +} + +impl ProvidesRegistryState for WaylandLock { + fn registry(&mut self) -> &mut RegistryState { &mut self.registry_state } + registry_handlers![OutputState, SeatState]; +} + +impl CompositorHandler for WaylandLock { + fn scale_factor_changed(&mut self, _conn: &Connection, _qh: &QueueHandle, _surface: &WlSurface, _new_factor: i32) {} + fn transform_changed(&mut self, _conn: &Connection, _qh: &QueueHandle, _surface: &WlSurface, _new_transform: wayland_client::protocol::wl_output::Transform) {} + fn frame(&mut self, _conn: &Connection, _qh: &QueueHandle, _surface: &WlSurface, _time: u32) {} + fn surface_enter(&mut self, _conn: &Connection, _qh: &QueueHandle, _surface: &WlSurface, _output: &WlOutput) {} + fn surface_leave(&mut self, _conn: &Connection, _qh: &QueueHandle, _surface: &WlSurface, _output: &WlOutput) {} +} + +impl OutputHandler for WaylandLock { + fn output_state(&mut self) -> &mut OutputState { &mut self.output_state } + fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle, _output: WlOutput) {} + fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle, _output: WlOutput) {} + fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle, _output: WlOutput) {} +} + +impl ShmHandler for WaylandLock { + fn shm_state(&mut self) -> &mut Shm { &mut self.shm_state } +} + +impl SessionLockHandler for WaylandLock { + fn locked(&mut self, _conn: &Connection, _qh: &QueueHandle, _session_lock: SessionLock) { + log::info!("Session LOCKED confirmed by compositor"); + } + + fn finished(&mut self, _conn: &Connection, _qh: &QueueHandle, _lock: SessionLock) { + log::info!("Session lock finished"); + self.exit = true; + } + + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + session_lock_surface: SessionLockSurface, + configure: SessionLockSurfaceConfigure, + _serial: u32, + ) { + let (width, height) = configure.new_size; + log::debug!("CONFIGURE callback: {}x{}", width, height); + + if let Ok(mut lock_manager) = self.lock_manager.lock() { + if let Some(locked_surface) = lock_manager.find_surface_by_wayland_surface(session_lock_surface.wl_surface()) { + locked_surface.resize(width as i32, height as i32); + locked_surface.set_configured(); + + let output = locked_surface.output(); + let output_id = Proxy::id(output); + let output_idx = self.outputs.iter().position(|o| Proxy::id(o) == output_id); + + if let Some(idx) = output_idx { + if let Some(bg) = self.captured_backgrounds.get(idx).and_then(|b| b.as_ref()) { + log::info!("Applying background for output {} (ID: {:?})", idx, output_id); + locked_surface.set_background(bg.clone()); + } + } + + locked_surface.update(); + let _ = locked_surface.commit(&mut self.pool); + } + } + } +} + +impl KeyboardHandler for WaylandLock { + fn enter(&mut self, _conn: &Connection, _qh: &QueueHandle, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _surface: &WlSurface, _serial: u32, _raw: &[u32], _keysyms: &[smithay_client_toolkit::seat::keyboard::Keysym]) {} + fn leave(&mut self, _conn: &Connection, _qh: &QueueHandle, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _surface: &WlSurface, _serial: u32) {} + fn press_key(&mut self, _conn: &Connection, _qh: &QueueHandle, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _serial: u32, event: KeyEvent) { + self.handle_key_event(event); + } + fn release_key(&mut self, _conn: &Connection, _qh: &QueueHandle, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _serial: u32, _event: KeyEvent) {} + fn update_modifiers(&mut self, _conn: &Connection, _qh: &QueueHandle, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _serial: u32, _modifiers: Modifiers, _layout: u32) {} +} + +impl SeatHandler for WaylandLock { + fn seat_state(&mut self) -> &mut SeatState { &mut self.seat_state } + fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: wayland_client::protocol::wl_seat::WlSeat) {} + fn new_capability(&mut self, _conn: &Connection, qh: &QueueHandle, seat: wayland_client::protocol::wl_seat::WlSeat, capability: smithay_client_toolkit::seat::Capability) { + if capability == smithay_client_toolkit::seat::Capability::Keyboard { + let _ = self.seat_state.get_keyboard_with_repeat(qh, &seat, None, self.loop_handle.clone(), Box::new(|_state, _kbd, _event| {})); + } + } + fn remove_capability(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: wayland_client::protocol::wl_seat::WlSeat, _capability: smithay_client_toolkit::seat::Capability) {} + fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: wayland_client::protocol::wl_seat::WlSeat) {} +} + +impl Dispatch for WaylandLock { + fn event(_state: &mut Self, _proxy: &ZwlrScreencopyManagerV1, _event: ::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle) {} +} + +impl Dispatch for WaylandLock { + fn event(state: &mut Self, frame: &ZwlrScreencopyFrameV1, event: ::Event, data: &CaptureData, _conn: &Connection, _qh: &QueueHandle) { + use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event; + match event { + Event::Buffer { format, width, height, stride } => { + let format = format.into_result().unwrap(); + let mut info = data.info.lock().unwrap(); + *info = Some(screenshot::BufferInfo { width, height, stride, format }); + match state.pool.create_buffer(width as i32, height as i32, stride as i32, format) { + Ok((buffer, _canvas)) => { + frame.copy(buffer.wl_buffer()); + *data.buffer.lock().unwrap() = Some(buffer); + } + Err(e) => log::error!("Screencopy: Buffer creation failed: {:?}", e), + } + } + Event::Flags { flags } => { *data.flags.lock().unwrap() = Some(flags.into_result().unwrap()); } + Event::Ready { .. } => { + log::info!("Screencopy: Ready for output {}", data.output_idx); + if let Some(mgr) = &state.screenshot_manager { + let buffer = data.buffer.lock().unwrap().take(); + let info = data.info.lock().unwrap().take(); + let flags = data.flags.lock().unwrap().take(); + if let (Some(buffer), Some(info), Some(flags)) = (buffer, info, flags) { + let handle = screenshot::ScreencopyBufferHandle { buffer, info, y_invert: flags.contains(Flags::YInvert) }; + if let Ok(surface) = mgr.buffer_to_surface(handle, &mut state.pool) { + let mut ss = Screenshot::new(surface); + let _ = ss.apply_effects(&state.config); + if data.output_idx < state.captured_backgrounds.len() { + state.captured_backgrounds[data.output_idx] = Some(ss.into_inner()); + } + } + } + } + state.pending_screenshots -= 1; + frame.destroy(); + } + Event::Failed => { + log::warn!("Screencopy: Failed for output {}", data.output_idx); + state.pending_screenshots -= 1; + frame.destroy(); + } + _ => {} + } + } +} + +impl wayland_client::Dispatch for WaylandLock { + fn event(_state: &mut Self, _proxy: &wayland_client::protocol::wl_registry::WlRegistry, _event: wayland_client::protocol::wl_registry::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle) {} +} + +smithay_client_toolkit::delegate_compositor!(WaylandLock); +smithay_client_toolkit::delegate_output!(WaylandLock); +smithay_client_toolkit::delegate_shm!(WaylandLock); +smithay_client_toolkit::delegate_seat!(WaylandLock); +smithay_client_toolkit::delegate_keyboard!(WaylandLock); +smithay_client_toolkit::delegate_registry!(WaylandLock); +smithay_client_toolkit::delegate_session_lock!(WaylandLock); +wayland_client::delegate_noop!(WaylandLock: ignore wayland_client::protocol::wl_buffer::WlBuffer); + fn main() -> Result<(), Box> { - setup_file_logging(); - log_to_file("Program starting"); - let config = Config::load(); - - if config.debug { - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Debug) - .init(); - } else { - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Debug) - .init(); - } + setup_file_logging(&config); + static LOGGER: DualLogger = DualLogger; + log::set_logger(&LOGGER).map(|()| log::set_max_level(log::LevelFilter::Debug))?; log::info!("Starting wayrustlock v{}", env!("CARGO_PKG_VERSION")); - log_to_file("Starting wayrustlock"); - log::info!("Attempting to lock Wayland session..."); - log_to_file("Attempting to lock Wayland session"); - log::warn!("WARNING: This is a screen locker. To kill it if stuck, use:"); - log::warn!(" Method 1: Switch to another TTY (Ctrl+Alt+F2) and kill the process"); - log::warn!(" Method 2: Use 'pkill -9 wayrustlock' from another terminal"); - log::warn!(" Method 3: Use 'killall wayrustlock'"); - log_to_file("Warnings printed"); - let lock_manager = Arc::new(Mutex::new(LockManager::new(config.clone()))); let ctrlc_exit = Arc::new(std::sync::atomic::AtomicBool::new(false)); - if let Err(e) = lock_wayland_session(config.clone(), lock_manager.clone(), ctrlc_exit) { - log::error!("Failed to lock Wayland session: {}", e); - log_to_file(&format!("Failed to lock Wayland session: {}", e)); - log::warn!("Falling back to demonstration mode"); - run_demonstration_mode(config, lock_manager)?; - } - - Ok(()) -} - -fn lock_wayland_session( - config: Config, - lock_manager: Arc>, - ctrlc_exit: Arc, -) -> Result<(), Box> { - use smithay_client_toolkit::reexports::calloop; - - use smithay_client_toolkit::{ - compositor::{CompositorHandler, CompositorState}, - output::{OutputHandler, OutputState}, - reexports::{ - calloop::{channel, timer, EventLoop, LoopHandle}, - calloop_wayland_source::WaylandSource, - }, - registry::{ProvidesRegistryState, RegistryState}, - registry_handlers, - seat::{ - keyboard::{KeyEvent, KeyboardHandler, Modifiers}, - pointer::PointerHandler, - SeatHandler, SeatState, - }, - session_lock::{ - SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface, - SessionLockSurfaceConfigure, - }, - shm::{slot::SlotPool, Shm, ShmHandler}, - }; - use std::time::Duration; - use wayland_client::{ - globals::registry_queue_init, - protocol::{wl_output, wl_surface}, - Connection, QueueHandle, - }; - - struct WaylandLock { - loop_handle: LoopHandle<'static, Self>, - conn: Connection, - compositor_state: CompositorState, - output_state: OutputState, - registry_state: RegistryState, - globals: GlobalList, - session_lock_state: SessionLockState, - seat_state: SeatState, - shm_state: Shm, - pool: SlotPool, - session_lock: Option, - lock_surfaces: Vec, - lock_manager: Arc>, - config: Config, - ctrlc_exit: Arc, - exit: bool, - auth_tx: Option>>, - unlocking: bool, - screenshot_manager: Option, - screenshot_frames: Vec>, - captured_backgrounds: Vec>, - locked_outputs: HashSet, - outputs: Vec, - lock_surface_outputs: Vec, - pending_screenshots: usize, - } - - impl SessionLockHandler for WaylandLock { - fn locked( - &mut self, - _conn: &Connection, - qh: &QueueHandle, - mut session_lock: SessionLock, - ) { - log::info!("==========================================="); - log::info!("Session LOCKED SUCCESSFULLY!"); - log::info!("==========================================="); - log_to_file("Session LOCKED - creating surfaces"); - eprintln!("SESSION LOCKED - creating lock surfaces"); - - let lock_ref = &mut session_lock; - - let outputs: Vec = self.output_state.outputs().collect(); - let output_count = outputs.len(); - - self.outputs = outputs.clone(); - self.screenshot_frames = vec![None; output_count]; - self.captured_backgrounds = vec![None; output_count]; - self.pending_screenshots = if self.config.screenshots { - output_count - } else { - 0 - }; - log::debug!("Pending screenshots: {}", self.pending_screenshots); - - for output in &outputs { - log_to_file(&format!("Creating lock surface for output")); - let surface = self.compositor_state.create_surface(qh); - let lock_surface = lock_ref.create_lock_surface(surface, output, qh); - self.lock_surfaces.push(lock_surface); - self.lock_surface_outputs.push(output.clone()); - self.locked_outputs.insert(output.clone()); - } - - log_to_file(&format!( - "Created {} lock surfaces", - self.lock_surfaces.len() - )); - - self.session_lock = Some(session_lock); - - if self.config.screenshots { - log::info!("Initializing screenshot capture..."); - match ScreenshotManager::new(&self.globals, qh) { - Ok(manager) => { - self.screenshot_manager = Some(manager); - log::info!("Screenshot manager initialized"); - - for (idx, output) in self.outputs.iter().enumerate() { - log::info!("Starting screenshot capture for output {}...", idx); - match self.screenshot_manager.as_mut().unwrap().capture_output( - output, - qh, - CaptureData::new(idx), - ) { - Ok(frame) => { - self.screenshot_frames[idx] = Some(frame); - log::debug!("Screenshot capture initiated for output {}", idx); - } - Err(e) => { - log::error!("Failed to capture output {}: {}", idx, e); - } - } - } - } - Err(e) => { - log::warn!("Screenshot capture not available: {}", e); - self.screenshot_manager = None; - } - } - } - } - - fn finished( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _session_lock: SessionLock, - ) { - if self.unlocking { - log::info!("✅ Session successfully unlocked - compositor confirmed"); - log_to_file("✅ Session successfully unlocked - compositor confirmed"); - self.unlocking = false; - } else { - log::warn!("Session finished without unlock request - lock denied or cancelled"); - log_to_file("Session finished without unlock request"); - } - self.session_lock = None; - self.exit = true; - log_to_file("Setting exit=true from finished callback"); - } - - fn configure( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - session_lock_surface: SessionLockSurface, - configure: SessionLockSurfaceConfigure, - _serial: u32, - ) { - let (width, height) = configure.new_size; - log::info!("==========================================="); - log::info!("CONFIGURE callback: {}x{}", width, height); - log::info!("==========================================="); - log_to_file(&format!("CONFIGURE: {}x{}", width, height)); - eprintln!("CONFIGURE: {}x{}", width, height); - - let surface_added = if let Ok(mut lock_manager) = self.lock_manager.lock() { - let surface_wl_id = session_lock_surface.wl_surface().id(); - let output_idx = self - .lock_surfaces - .iter() - .position(|s| s.wl_surface().id() == surface_wl_id); - - if let Some(idx) = output_idx { - if idx < self.lock_surface_outputs.len() { - let output = &self.lock_surface_outputs[idx]; - let (width, height) = configure.new_size; - lock_manager.add_surface(width as i32, height as i32, output.clone()) - } else { - log::error!( - "lock_surface_outputs index out of bounds: {} vs {}", - idx, - self.lock_surface_outputs.len() - ); - false - } - } else { - log::error!("Could not find lock surface in self.lock_surfaces"); - false - } - } else { - false - }; - - if !surface_added { - log::error!("Failed to add surface to lock manager"); - session_lock_surface.wl_surface().commit(); - return; - } - - log::debug!("Surface added to lock manager"); - - if let Ok(mut lock_manager) = self.lock_manager.lock() { - let surface_count = lock_manager.surface_count(); - if surface_count == 0 { - log::error!("No surfaces in lock manager"); - session_lock_surface.wl_surface().commit(); - return; - } - - if let Some(locked_surface) = lock_manager.get_surface_mut(surface_count - 1) { - locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone()); - - if self.config.screenshots { - let output = locked_surface.output(); - let output_idx = self.outputs.iter().position(|o| o.id() == output.id()); - - if let Some(idx) = output_idx { - if let Some(background) = self - .captured_backgrounds - .get_mut(idx) - .and_then(|b| b.take()) - { - log::info!( - "Using captured screenshot background for output {}", - idx - ); - locked_surface.set_background(background); - } else { - log::warn!("No screenshot available for output {}, will render without background", idx); - } - } else { - log::warn!( - "Output not found in self.outputs, cannot assign screenshot" - ); - } - } - } - - if let Some(locked_surface) = lock_manager.get_surface_mut(surface_count - 1) { - locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone()); - - // Render the surface - locked_surface.renderer.render(); - - match locked_surface.renderer.get_pixel_data() { - Ok(pixel_data) => { - let (_renderer_width, _renderer_height, stride) = - locked_surface.renderer.surface_info(); - let actual_width = width as i32; - let actual_height = height as i32; - let actual_stride = stride; - - // Create a buffer from the pool - match self.pool.create_buffer( - actual_width, - actual_height, - actual_stride, - wayland_client::protocol::wl_shm::Format::Argb8888, - ) { - Ok((buffer, canvas)) => { - // Copy pixel data to the buffer - let data_len = canvas.len(); - let copy_len = pixel_data.len().min(data_len); - canvas[..copy_len].copy_from_slice(&pixel_data[..copy_len]); - - // Damage the entire surface - session_lock_surface.wl_surface().damage_buffer( - 0, - 0, - actual_width, - actual_height, - ); - - // Attach buffer and commit - if let Err(e) = - buffer.attach_to(session_lock_surface.wl_surface()) - { - log::error!("Failed to attach buffer: {:?}", e); - } else { - session_lock_surface.wl_surface().commit(); - log::debug!( - "Surface committed with buffer {}x{}", - actual_width, - actual_height - ); - } - } - Err(e) => { - log::error!("Failed to create buffer from pool: {:?}", e); - session_lock_surface.wl_surface().commit(); - } - } - } - Err(e) => { - log::error!("Failed to get pixel data from Cairo surface: {:?}", e); - session_lock_surface.wl_surface().commit(); - } - } - } - } - } - } - - impl CompositorHandler for WaylandLock { - fn scale_factor_changed( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _surface: &wl_surface::WlSurface, - _new_factor: i32, - ) { - log::debug!("Scale factor changed"); - } - - fn transform_changed( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _surface: &wl_surface::WlSurface, - _new_transform: wl_output::Transform, - ) { - log::debug!("Transform changed"); - } - - fn frame( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - surface: &wl_surface::WlSurface, - _time: u32, - ) { - if let Ok(mut lock_manager) = self.lock_manager.lock() { - lock_manager.update(); - - if let Some(_locked_surface) = lock_manager.find_surface_by_wayland_surface(surface) - { - // For now, just log that we would update the surface - // TODO: Implement proper buffer creation for animation - log::debug!("Frame update for surface"); - } - } - } - - fn surface_enter( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _surface: &wl_surface::WlSurface, - _output: &wl_output::WlOutput, - ) { - log::debug!("Surface entered"); - } - - fn surface_leave( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _surface: &wl_surface::WlSurface, - _output: &wl_output::WlOutput, - ) { - log::debug!("Surface left"); - } - } - - impl OutputHandler for WaylandLock { - fn output_state(&mut self) -> &mut OutputState { - &mut self.output_state - } - - fn new_output( - &mut self, - _conn: &Connection, - qh: &QueueHandle, - output: wl_output::WlOutput, - ) { - log::info!("New output detected: creating lock surface"); - if self.locked_outputs.contains(&output) { - log::debug!("Output already has lock surface, ignoring"); - return; - } - if let Some(ref session_lock) = self.session_lock { - let surface = self.compositor_state.create_surface(qh); - let _lock_surface = session_lock.create_lock_surface(surface, &output, qh); - self.locked_outputs.insert(output.clone()); - self.lock_surface_outputs.push(output.clone()); - } - } - - fn update_output( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _output: wl_output::WlOutput, - ) { - log::debug!("Output updated"); - } - - fn output_destroyed( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _output: wl_output::WlOutput, - ) { - log::info!("Output destroyed"); - } - } - - impl ShmHandler for WaylandLock { - fn shm_state(&mut self) -> &mut Shm { - &mut self.shm_state - } - } - - impl KeyboardHandler for WaylandLock { - fn enter( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, - _surface: &wl_surface::WlSurface, - _serial: u32, - _keys: &[u32], - _layout_keysyms: &[smithay_client_toolkit::seat::keyboard::Keysym], - ) { - log::info!("Keyboard entered surface (serial: {})", _serial); - log_to_file(&format!("Keyboard entered surface (serial: {})", _serial)); - } - - fn leave( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, - _surface: &wl_surface::WlSurface, - _serial: u32, - ) { - log::info!("Keyboard left surface (serial: {})", _serial); - log_to_file(&format!("Keyboard left surface (serial: {})", _serial)); - } - - fn press_key( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, - _serial: u32, - event: KeyEvent, - ) { - log::info!( - "Key pressed (serial: {}, keysym: {:?})", - _serial, - event.keysym - ); - log_to_file(&format!("Key pressed: keysym={:?}", event.keysym)); - self.handle_key_event(event); - } - - fn release_key( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, - _serial: u32, - _event: KeyEvent, - ) { - log::debug!("Key released (serial: {})", _serial); - } - - fn update_modifiers( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, - _serial: u32, - _modifiers: Modifiers, - _layout: u32, - ) { - log::debug!("Modifiers updated (serial: {})", _serial); - } - - fn update_keymap( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, - _keymap: smithay_client_toolkit::seat::keyboard::Keymap<'_>, - ) { - log::info!("Keymap updated"); - log_to_file("Keymap updated"); - } - } - - impl PointerHandler for WaylandLock { - fn pointer_frame( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _pointer: &wayland_client::protocol::wl_pointer::WlPointer, - _events: &[smithay_client_toolkit::seat::pointer::PointerEvent], - ) { - // Screen locker doesn't need pointer events - } - } - - impl WaylandLock { - fn handle_key_event(&mut self, event: KeyEvent) { - use crate::input::InputAction; - use smithay_client_toolkit::seat::keyboard::Keysym; - - log_to_file(&format!( - "handle_key_event called: keysym={:?}", - event.keysym - )); - log::debug!("Key event: {:?}", event.keysym); - - if event.keysym == Keysym::Return { - log::info!("Enter pressed - submitting password for authentication"); - log_to_file("ENTER pressed - submitting password"); - - if let Ok(mut lock_manager) = self.lock_manager.lock() { - if let Some(InputAction::SubmitPassword(password)) = - lock_manager.handle_key_event(event.clone()) - { - // Send password to auth thread for processing - if let Some(tx) = &self.auth_tx { - let _ = tx.send(password); - } - log::debug!("Password submitted for authentication"); - } - } - } else { - // Distribute to all surfaces (BackSpace, characters, etc.) - let _ = self - .lock_manager - .lock() - .map(|mut lm| lm.handle_key_event(event)); - } - } - - /// Handle authentication result from the auth thread - fn handle_auth_result(&mut self, success: bool) { - if success { - log::info!("✅ Authentication successful - unlocking session"); - log_to_file("✅ Authentication successful - unlocking session"); - - // Call unlock on the session_lock, but keep it to receive finished event - if let Some(session_lock) = &self.session_lock { - session_lock.unlock(); - self.unlocking = true; - log_to_file("Unlock requested - waiting for finished event"); - } else { - log::error!("No session_lock available to unlock!"); - } - } else { - log::warn!("❌ Authentication failed - wrong password"); - log_to_file("❌ Authentication failed - wrong password"); - // Show wrong password feedback on all surfaces - if let Ok(mut lock_manager) = self.lock_manager.lock() { - for surface in &mut lock_manager.surfaces { - surface.show_wrong_password(); - } - } - } - } - } - - impl SeatHandler for WaylandLock { - fn seat_state(&mut self) -> &mut SeatState { - &mut self.seat_state - } - - fn new_seat( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _seat: wayland_client::protocol::wl_seat::WlSeat, - ) { - log::debug!("New seat detected"); - } - - fn new_capability( - &mut self, - _conn: &Connection, - qh: &QueueHandle, - seat: wayland_client::protocol::wl_seat::WlSeat, - capability: smithay_client_toolkit::seat::Capability, - ) { - log::info!("New capability: {:?}", capability); - log_to_file(&format!("New capability: {:?}", capability)); - - if capability == smithay_client_toolkit::seat::Capability::Keyboard { - log::info!("Setting up keyboard"); - log_to_file("Setting up keyboard - trying to get keyboard"); - - match self.seat_state.get_keyboard_with_repeat( - qh, - &seat, - None, - self.loop_handle.clone(), - Box::new(|_state, _wl_kbd, event| { - log::info!("Keyboard repeat event: {:?}", event); - log_to_file(&format!("Keyboard repeat: {:?}", event)); - }), - ) { - Ok(_keyboard) => { - log::info!("Keyboard created successfully"); - log_to_file("Keyboard created successfully"); - } - Err(e) => { - log::error!("Failed to create keyboard: {:?}", e); - log_to_file(&format!("Failed to create keyboard: {:?}", e)); - } - } - } - } - - fn remove_capability( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _seat: wayland_client::protocol::wl_seat::WlSeat, - capability: smithay_client_toolkit::seat::Capability, - ) { - log::debug!("Capability removed: {:?}", capability); - } - - fn remove_seat( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _seat: wayland_client::protocol::wl_seat::WlSeat, - ) { - log::debug!("Seat removed"); - } - } - - impl ProvidesRegistryState for WaylandLock { - fn registry(&mut self) -> &mut RegistryState { - &mut self.registry_state - } - registry_handlers![OutputState, SeatState,]; - } - - smithay_client_toolkit::delegate_compositor!(WaylandLock); - smithay_client_toolkit::delegate_output!(WaylandLock); - smithay_client_toolkit::delegate_session_lock!(WaylandLock); - smithay_client_toolkit::delegate_registry!(WaylandLock); - smithay_client_toolkit::delegate_shm!(WaylandLock); - smithay_client_toolkit::delegate_seat!(WaylandLock); - smithay_client_toolkit::delegate_keyboard!(WaylandLock); - smithay_client_toolkit::delegate_pointer!(WaylandLock); - - impl wayland_client::Dispatch for WaylandLock { - fn event( - state: &mut Self, - _manager: &ZwlrScreencopyManagerV1, - _event: ::Event, - _data: &(), - _conn: &Connection, - _qh: &QueueHandle, - ) { - // No events expected from the manager - } - } - - impl wayland_client::Dispatch for WaylandLock { - fn event( - state: &mut Self, - frame: &ZwlrScreencopyFrameV1, - event: ::Event, - _data: &CaptureData, - _conn: &Connection, - qh: &QueueHandle, - ) { - let output_idx = _data.output_idx; - log::debug!("Screencopy event for output {}: {:?}", output_idx, event); - - match event { - wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Buffer { - format, - width, - height, - stride, - } => { - log::debug!("Buffer event: {}x{}, stride={}, format={:?}", width, height, stride, format); - - // Extract format value from the compositor's Buffer event - let format_value = match format { - wayland_client::WEnum::Value(f) => f, - wayland_client::WEnum::Unknown(_) => { - log::error!("Unknown format value"); - return; - } - }; - - // Store buffer info including the actual format - if let Ok(mut info_guard) = _data.info.lock() { - *info_guard = Some(screenshot::BufferInfo { - width, - height, - stride, - format: format_value, - }); - } - - // Create SHM buffer with the EXACT format the compositor specified - if let Ok(mut buffer_guard) = _data.buffer.lock() { - if let Ok((buf, _canvas)) = state.pool.create_buffer( - i32::try_from(width).unwrap(), - i32::try_from(height).unwrap(), - i32::try_from(stride).unwrap(), - format_value, - ) { - // Store the buffer for later use in Ready event - *buffer_guard = Some(buf); - log::debug!("Created SHM buffer for screencopy with format {:?}", format_value); - // Immediately send copy request (protocol requires this after buffer creation) - if let Some(buffer) = buffer_guard.as_ref() { - frame.copy(buffer.wl_buffer()); - log::debug!("Sent copy request for screenshot"); - } - } else { - log::error!("Failed to create SHM buffer"); - } - } - } - wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Flags { flags } => { - log::debug!("Flags: {:?}", flags); - if let Ok(mut flags_guard) = _data.flags.lock() { - let flags_value = match flags { - wayland_client::WEnum::Value(f) => f, - wayland_client::WEnum::Unknown(_) => { - log::warn!("Unknown flags value"); - return; - } - }; - *flags_guard = Some(flags_value); - } - } - wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Ready { - tv_sec_hi: _, - tv_sec_lo: _, - tv_nsec: _ - } => { - log::info!("Ready event: processing screenshot for output {}", output_idx); - - // Extract all data from CaptureData - let info_opt = _data.info.lock().ok().and_then(|i| i.clone()); - let buffer_opt = _data.buffer.lock().ok().and_then(|mut b| b.take()); - let flags_opt = _data.flags.lock().ok().and_then(|f| f.clone()); - - if let (Some(info), Some(buffer), Some(flags)) = (info_opt, buffer_opt, flags_opt) { - // Determine if Y-inversion is needed (Y_INVERT flag is bit 0) - let y_invert = flags.bits() & 1 != 0; - - let handle = screenshot::ScreencopyBufferHandle { - buffer, - info: screenshot::BufferInfo { - width: info.width, - height: info.height, - stride: info.stride, - format: info.format, - }, - y_invert, - }; - - match state.screenshot_manager.as_ref().unwrap().buffer_to_surface(handle, &mut state.pool) { - Ok(surface) => { - log::info!("✓ Converted screenshot for output {}: {}x{}", output_idx, surface.width(), surface.height()); - // Apply effects if configured - let mut screenshot = Screenshot::new(surface); - if let Err(e) = screenshot.apply_effects(&state.config) { - log::error!("Failed to apply effects: {}", e); - } - let surface = screenshot.into_inner(); - log::info!("✓ Screenshot ready for output {}", output_idx); - - // Save screenshot to file for debugging if debug mode enabled - if state.config.debug { - let path = format!("/tmp/wayrustlock_output{}.png", output_idx); - match std::fs::File::create(&path) { - Ok(mut file) => { - if let Err(e) = surface.write_to_png(&mut file) { - log::error!("Failed to write PNG to {}: {}", path, e); - } else { - log::info!("✓ Saved screenshot to {}", path); - } - } - Err(e) => { - log::error!("Failed to create file {}: {}", path, e); - } - } - } - - if output_idx < state.outputs.len() { - let output = &state.outputs[output_idx]; - if let Ok(mut lock_manager) = state.lock_manager.lock() { - if let Some(locked_surface) = lock_manager.find_surface_by_output(output) { - log::info!("✓ Setting background on locked surface for output {}", output_idx); - locked_surface.set_background(surface); - } else { - log::warn!("⚠ No locked surface found for output {}, storing in captured_backgrounds", output_idx); - state.captured_backgrounds[output_idx] = Some(surface); - } - } else { - log::warn!("⚠ Lock manager poisoned, storing in captured_backgrounds for output {}", output_idx); - state.captured_backgrounds[output_idx] = Some(surface); - } - } else { - log::error!("output_idx {} out of bounds (outputs len {})", output_idx, state.outputs.len()); - } - } - Err(e) => { - log::error!("✗ Failed to convert buffer to surface: {}", e); - } - } - } else { - log::error!("Missing buffer data for Ready event"); - } - - // Decrement pending screenshots count - if state.pending_screenshots > 0 { - state.pending_screenshots -= 1; - log::debug!("Screenshot ready, pending_screenshots now: {}", state.pending_screenshots); - if state.pending_screenshots == 0 { - log::info!("All screenshots captured, lock UI will now be displayed"); - } - } - - // Clean up frame - frame.destroy(); - } - wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Failed => { - log::error!("Screenshot capture failed for output {}", output_idx); - // Decrement pending screenshots count even on failure - if state.pending_screenshots > 0 { - state.pending_screenshots -= 1; - log::debug!("Screenshot failed, pending_screenshots now: {}", state.pending_screenshots); - if state.pending_screenshots == 0 { - log::info!("All screenshot attempts completed (with failures), lock UI will now be displayed"); - } - } - frame.destroy(); - } - _ => {} - } - } - } - let conn = Connection::connect_to_env()?; - let (globals, event_queue) = registry_queue_init(&conn)?; + let (globals, mut event_queue) = wayland_client::globals::registry_queue_init::(&conn)?; let qh: QueueHandle = event_queue.handle(); - let mut event_loop: EventLoop = EventLoop::try_new()?; - // Create the authentication loop in a separate thread - let (auth_tx, auth_rx) = auth::create_and_run_auth_loop(); + let shm_state = Shm::bind(&globals, &qh).map_err(|_| "wl_shm not supported")?; + let pool = SlotPool::new(1920 * 1080 * 4, &shm_state)?; + + let (auth_tx_actual, auth_feedback_rx_actual) = auth::create_and_run_auth_loop(); + let mut event_loop: EventLoop = EventLoop::try_new()?; let mut state = WaylandLock { loop_handle: event_loop.handle(), - conn: conn.clone(), - lock_manager, - config, + lock_manager: lock_manager.clone(), + config: config.clone(), ctrlc_exit: ctrlc_exit.clone(), - auth_tx: Some(auth_tx), + auth_tx: Some(auth_tx_actual), compositor_state: CompositorState::bind(&globals, &qh)?, output_state: OutputState::new(&globals, &qh), registry_state: RegistryState::new(&globals), session_lock_state: SessionLockState::new(&globals, &qh), seat_state: SeatState::new(&globals, &qh), - shm_state: Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?, - pool: SlotPool::new( - 256 * 1024 * 1024, // 256 MB pool to support high-resolution displays and screenshots - &Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?, - ) - .map_err(|e| format!("Failed to create slot pool: {:?}", e))?, - globals, + shm_state, + pool, session_lock: None, lock_surfaces: Vec::new(), - exit: false, - unlocking: false, - screenshot_manager: None, + outputs: Vec::new(), screenshot_frames: Vec::new(), captured_backgrounds: Vec::new(), - locked_outputs: HashSet::new(), - outputs: Vec::new(), - lock_surface_outputs: Vec::new(), pending_screenshots: 0, + exit: false, + unlocking: false, + screenshot_manager: ScreenshotManager::new(&globals, &qh).ok(), }; - state.session_lock = Some( - state - .session_lock_state - .lock(&qh) - .map_err(|_| "ext-session-lock-v1 protocol not supported by compositor")?, - ); + event_queue.blocking_dispatch(&mut state)?; - log::info!("Session lock requested, waiting for compositor..."); - log_to_file("Session lock requested"); - eprintln!("Session lock requested - running event loop"); + let _wayland_source = WaylandSource::new(conn.clone(), event_queue).insert(event_loop.handle())?; - WaylandSource::new(conn, event_queue).insert(event_loop.handle())?; + event_loop.handle().insert_source(auth_feedback_rx_actual, |event, _, state| { + if let calloop::channel::Event::Msg(success) = event { + state.handle_auth_result(success); + if success { state.lock_surfaces.clear(); } + } + })?; - // Insert the authentication channel into the event loop - event_loop.handle().insert_source( - auth_rx, - |event: channel::Event, _metadata, wayland_lock| { - if let channel::Event::Msg(result) = event { - wayland_lock.handle_auth_result(result); - } - }, - )?; + let timer = calloop::timer::Timer::from_duration(Duration::from_millis(16)); + event_loop.handle().insert_source(timer, |_, _, state| { + if state.unlocking { + return calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(100)); + } - // Insert a timer to update and commit surfaces at 60fps - let render_timer = timer::Timer::from_duration(Duration::from_millis(16)); - event_loop.handle().insert_source( - render_timer, - |_event, _metadata, state: &mut WaylandLock| { - log::debug!( - "Timer tick, pending_screenshots: {}", - state.pending_screenshots - ); - // Skip rendering if we're still waiting for screenshots - if state.pending_screenshots > 0 { - log::debug!( - "Still waiting for {} screenshots, skipping render", - state.pending_screenshots - ); - return calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(16)); - } + if let Ok(mut lm) = state.lock_manager.lock() { + lm.update(); + for surface in &mut lm.surfaces { let _ = surface.commit(&mut state.pool); } + } + if state.exit { calloop::timer::TimeoutAction::Drop } else { calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(16)) } + })?; - // Update lock manager (renders to Cairo surfaces) - if let Ok(mut lock_manager) = state.lock_manager.lock() { - lock_manager.update(); - // Commit each surface that has a Wayland surface - for surface in lock_manager.surfaces.iter_mut() { - if let Some(_wl_surface) = surface.wayland_surface() { - if let Err(e) = surface.commit(&mut state.pool) { - log::error!("Failed to commit surface: {:?}", e); - } else { - log::debug!("Committed surface"); - } - } else { - log::debug!("Surface has no wayland_surface"); - } + if state.config.screenshots { + state.outputs = state.output_state.outputs().collect(); + log::info!("Capturing screenshots for {} outputs...", state.outputs.len()); + state.screenshot_frames = vec![None; state.outputs.len()]; + state.captured_backgrounds = vec![None; state.outputs.len()]; + state.pending_screenshots = state.outputs.len(); + + for (i, output) in state.outputs.iter().enumerate() { + if let Some(mgr) = &state.screenshot_manager { + let data = CaptureData::new(i); + match mgr.capture_output(output, &qh, data) { + Ok(frame) => state.screenshot_frames[i] = Some(frame), + Err(e) => { log::error!("Screencopy failed for output {}: {:?}", i, e); state.pending_screenshots -= 1; } } - } else { - log::error!("Failed to lock lock_manager mutex - poisoned?"); - } - // Reschedule timer to fire again in 16ms (60fps) - calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(16)) - }, - )?; + } else { state.pending_screenshots -= 1; } + } + + let start = std::time::Instant::now(); + while state.pending_screenshots > 0 && start.elapsed() < Duration::from_secs(2) { + event_loop.dispatch(Duration::from_millis(50), &mut state)?; + } + log::info!("Screenshot capture phase complete"); + } - log_to_file("Starting event loop"); - eprintln!("Starting event loop - press Enter to unlock"); + log::info!("Attempting to lock Wayland session..."); + let session_lock = state.session_lock_state.lock(&qh).map_err(|e| { log::error!("Lock failed: {}", e); e })?; + + let outputs_to_lock: Vec = state.output_state.outputs().collect(); + for output in outputs_to_lock { + let surface = state.compositor_state.create_surface(&qh); + let (width, height) = state.get_output_dimensions(&output); + let lock_surface = session_lock.create_lock_surface(surface.clone(), &output, &qh); + state.lock_surfaces.push(lock_surface); + if !state.outputs.contains(&output) { state.outputs.push(output.clone()); } + if let Ok(mut lm) = state.lock_manager.lock() { + lm.add_surface(width, height, output.clone()); + let count = lm.surface_count(); + if let Some(ls) = lm.get_surface_mut(count - 1) { ls.set_wayland_surface(surface); } + } + } + state.session_lock = Some(session_lock); while !state.exit && !state.ctrlc_exit.load(std::sync::atomic::Ordering::SeqCst) { event_loop.dispatch(Duration::from_millis(16), &mut state)?; } - log_to_file("Event loop exited"); - log::info!("Event loop exited, exit={}", state.exit); - - log_to_file("Function completed"); - Ok(()) -} - -fn run_demonstration_mode( - _config: Config, - lock_manager: Arc>, -) -> Result<(), Box> { - use std::time::Duration; - - println!("wayrustlock - Wayland Screen Locker (Demonstration Mode)"); - println!("========================================================="); - println!("Note: Running in demonstration mode because:"); - println!("1. Not running on Wayland compositor, OR"); - println!("2. ext-session-lock-v1 protocol not available, OR"); - println!("3. Wayland connection failed"); - println!(); - println!("To actually lock your screen, ensure:"); - println!("1. You're running on sway, niri, or another Wayland compositor"); - println!("2. The compositor supports ext-session-lock-v1 protocol"); - println!("3. You have the required Wayland libraries installed"); - println!(); - - { - let mut lock_manager = lock_manager.lock().unwrap(); - log::info!( - "Initialized {} lock surface(s)", - lock_manager.surface_count() - ); - } - - log::info!("Demonstration mode: Press Ctrl+C to exit"); - - // For demonstration mode, just run for a short time then exit - let _running = Arc::new(std::sync::atomic::AtomicBool::new(true)); - - std::thread::sleep(Duration::from_secs(30)); - - log::info!("Exiting demonstration mode"); + log::info!("Exiting wayrustlock"); Ok(()) } diff --git a/src/render.rs b/src/render.rs index 4ba5ddc..5a9f629 100644 --- a/src/render.rs +++ b/src/render.rs @@ -2,7 +2,6 @@ use cairo::{Context, Format, ImageSurface}; use std::time::Instant; use crate::config::Config; -use crate::util::Color; /// Cairo-based renderer for the lock screen pub struct Renderer { @@ -16,23 +15,18 @@ pub struct Renderer { key_highlight_shown: bool, wrong_password_start: Option, key_highlight_start: Option, + key_highlight_angle: f64, background: Option, password_display: String, + uptime_cache: String, + last_uptime_update: Option, + caps_lock: bool, } impl Renderer { - /// Convert color tuple to Color struct - fn tuple_to_color(&self, color: (f64, f64, f64, f64)) -> Color { - Color { - r: (color.0 * 255.0) as u8, - g: (color.1 * 255.0) as u8, - b: (color.2 * 255.0) as u8, - a: (color.3 * 255.0) as u8, - } - } - /// Create a new renderer with the given dimensions and configuration pub fn new(width: i32, height: i32, config: Config) -> Self { + log::debug!("Renderer::new({}, {}, ...) called", width, height); let surface = ImageSurface::create(Format::ARgb32, width, height) .expect("Failed to create Cairo surface"); let context = Context::new(&surface).expect("Failed to create Cairo context"); @@ -48,13 +42,18 @@ impl Renderer { key_highlight_shown: false, wrong_password_start: None, key_highlight_start: None, + key_highlight_angle: 0.0, background: None, password_display: String::new(), + uptime_cache: String::new(), + last_uptime_update: None, + caps_lock: false, } } /// Resize the renderer to new dimensions pub fn resize(&mut self, width: i32, height: i32) { + log::debug!("Renderer::resize({}, {}) called", width, height); self.width = width; self.height = height; @@ -83,6 +82,15 @@ impl Renderer { pub fn show_key_highlight(&mut self) { self.key_highlight_shown = true; self.key_highlight_start = Some(Instant::now()); + + // Generate ONE random angle for this highlight + use std::time::SystemTime; + let seed = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + let random_val = seed.wrapping_mul(6364136223846793005).wrapping_add(1); + self.key_highlight_angle = ((random_val % 360) as f64).to_radians(); } /// Set the password display string (masked) @@ -92,61 +100,42 @@ impl Renderer { /// Render the current frame pub fn render(&mut self) { - log::info!( - "Renderer::render() called, background: {}", - self.background.is_some() - ); - - // Clear the surface - draw a VISIBLE color (dark gray) instead of black - self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0); + // Clear the surface + self.context.new_path(); + self.context.set_source_rgba(0.0, 0.0, 0.0, 1.0); self.context.paint().expect("Failed to clear surface"); - // Draw background if available + // Draw background if let Some(ref background) = self.background { - let size = (background.width(), background.height()); - log::info!( - "✓ Drawing background ({}x{}, fade_alpha: {})", - size.0, - size.1, - self.fade_alpha - ); + self.context.new_path(); self.context .set_source_surface(background, 0.0, 0.0) - .expect("Failed to set background source"); + .expect("Failed to set source"); self.context .paint_with_alpha(self.fade_alpha) - .expect("Failed to draw background"); - log::info!("✓ Background drawn successfully"); - } else { - // Draw solid color background (dark gray visible color) - log::warn!("✗ No background available - rendering solid gray!"); - self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0); - self.context - .paint() - .expect("Failed to draw solid background"); + .expect("Failed to paint"); } - // Draw clock if enabled - if self.config.clock { - self.draw_clock(); - } - - // Draw indicator if enabled if self.config.indicator { self.draw_indicator(); } - // Draw password display (if not empty) + if self.config.clock { + self.draw_clock(); + } + if !self.password_display.is_empty() { self.draw_password_display(); } - // Draw wrong password feedback if active + if self.caps_lock { + self.draw_caps_lock_indicator(); + } + if self.wrong_password_shown { self.draw_wrong_password_feedback(); } - // Draw key highlight feedback if active if self.key_highlight_shown { self.draw_key_highlight_feedback(); } @@ -154,96 +143,79 @@ impl Renderer { self.update_feedback_timers(); } - /// Get the rendered image surface - pub fn as_image_surface(&self) -> &ImageSurface { - &self.surface - } - /// Get raw pixel data from the surface (ARGB32 format) pub fn get_pixel_data(&self) -> Result, cairo::BorrowError> { let stride = self.surface.stride() as usize; let height = self.height as usize; - let mut data = vec![0u8; stride * height]; self.surface.with_data(|src| { data.copy_from_slice(src); })?; - Ok(data) } - /// Get surface dimensions and stride pub fn surface_info(&self) -> (i32, i32, i32) { (self.width, self.height, self.surface.stride()) } - /// Draw the clock in the center of the screen + fn update_uptime(&mut self) { + let now = Instant::now(); + if let Some(last) = self.last_uptime_update { + if now.duration_since(last).as_secs() < 10 { + return; + } + } + let uptime_secs = std::fs::read_to_string("/proc/uptime") + .ok() + .and_then(|s| s.split_whitespace().next()?.parse::().ok()) + .unwrap_or(0.0) as u64; + self.uptime_cache = format!("up {}h {}m", uptime_secs / 3600, (uptime_secs % 3600) / 60); + self.last_uptime_update = Some(now); + } + fn draw_clock(&self) { use chrono::Local; - let now = Local::now(); let time_str = now.format("%H:%M").to_string(); let date_str = now.format("%A, %B %d").to_string(); + let center_x = self.width as f64 / 2.0; + let center_y = self.height as f64 / 2.0; - self.context.set_font_size(72.0); + self.context.new_path(); self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha); - - // Center the text - let extents = self - .context - .text_extents(&time_str) - .expect("Failed to get text extents"); - let x = (self.width as f64 - extents.width()) / 2.0; - let y = (self.height as f64 / 2.0) - extents.height() / 2.0; - - self.context.move_to(x, y); + self.context.set_font_size(48.0); + let te = self.context.text_extents(&time_str).unwrap(); self.context - .show_text(&time_str) - .expect("Failed to draw time"); + .move_to(center_x - te.width() / 2.0, center_y + te.height() / 4.0); + self.context.show_text(&time_str).unwrap(); - // Draw date below time - self.context.set_font_size(24.0); - let date_extents = self - .context - .text_extents(&date_str) - .expect("Failed to get date extents"); - let date_x = (self.width as f64 - date_extents.width()) / 2.0; - let date_y = y + extents.height() + 20.0; + self.context.new_path(); + self.context.set_font_size(14.0); + let de = self.context.text_extents(&date_str).unwrap(); + self.context.move_to( + center_x - de.width() / 2.0, + center_y + te.height() / 4.0 + 25.0, + ); + self.context.show_text(&date_str).unwrap(); - self.context.move_to(date_x, date_y); - self.context - .show_text(&date_str) - .expect("Failed to draw date"); + self.context.new_path(); + let ue = self.context.text_extents(&self.uptime_cache).unwrap(); + self.context.move_to( + center_x - ue.width() / 2.0, + center_y + te.height() / 4.0 + 43.0, + ); + self.context.show_text(&self.uptime_cache).unwrap(); } - /// Draw the password indicator ring fn draw_indicator(&self) { let center_x = self.width as f64 / 2.0; let center_y = self.height as f64 / 2.0; let radius = self.config.indicator_radius as f64; let thickness = self.config.indicator_thickness as f64; - // Draw outer ring - let ring_color = self.tuple_to_color(self.config.ring_color); - self.context.set_source_rgba( - ring_color.r as f64 / 255.0, - ring_color.g as f64 / 255.0, - ring_color.b as f64 / 255.0, - ring_color.a as f64 / 255.0 * self.fade_alpha, - ); - self.context.set_line_width(thickness); - self.context - .arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI); - self.context.stroke().expect("Failed to draw ring"); - - // Draw inside fill - let inside_color = self.tuple_to_color(self.config.inside_color); - self.context.set_source_rgba( - inside_color.r as f64 / 255.0, - inside_color.g as f64 / 255.0, - inside_color.b as f64 / 255.0, - inside_color.a as f64 / 255.0 * self.fade_alpha, - ); + self.context.new_path(); + let (r, g, b, a) = self.config.inside_color; + self.context.set_source_rgba(r, g, b, a * self.fade_alpha); self.context.arc( center_x, center_y, @@ -251,62 +223,75 @@ impl Renderer { 0.0, 2.0 * std::f64::consts::PI, ); - self.context.fill().expect("Failed to fill inside"); + self.context.fill().unwrap(); - // Draw separator line - let separator_color = self.tuple_to_color(self.config.separator_color); - if separator_color.a > 0 { - self.context.set_source_rgba( - separator_color.r as f64 / 255.0, - separator_color.g as f64 / 255.0, - separator_color.b as f64 / 255.0, - separator_color.a as f64 / 255.0 * self.fade_alpha, + let (lr, lg, lb, la) = self.config.line_color; + if la > 0.0 { + self.context.new_path(); + self.context + .set_source_rgba(lr, lg, lb, la * self.fade_alpha); + self.context.set_line_width(1.0); + self.context.arc( + center_x, + center_y, + radius - thickness / 2.0, + 0.0, + 2.0 * std::f64::consts::PI, ); + self.context.stroke().unwrap(); + } + + self.context.new_path(); + let (r, g, b, a) = self.config.ring_color; + self.context.set_source_rgba(r, g, b, a * self.fade_alpha); + self.context.set_line_width(thickness); + self.context + .arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI); + self.context.stroke().unwrap(); + + let (r, g, b, a) = self.config.separator_color; + if a > 0.0 { + self.context.new_path(); + self.context.set_source_rgba(r, g, b, a * self.fade_alpha); self.context.set_line_width(1.0); self.context.move_to(center_x - radius, center_y); self.context.line_to(center_x + radius, center_y); - self.context.stroke().expect("Failed to draw separator"); + self.context.stroke().unwrap(); } } - /// Draw the password display (masked characters) fn draw_password_display(&self) { - if self.password_display.is_empty() { - return; - } - - // Position: below the indicator ring (or centered if no indicator) let center_x = self.width as f64 / 2.0; let center_y = self.height as f64 / 2.0; let radius = self.config.indicator_radius as f64; - let thickness = self.config.indicator_thickness as f64; - - // Place password text below the ring - let text_y = center_y + radius + thickness + 40.0; // 40px below ring - - self.context.set_font_size(36.0); + self.context.new_path(); + self.context.set_font_size(32.0); self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha); - - // Center the text - let extents = self - .context - .text_extents(&self.password_display) - .expect("Failed to get password text extents"); - let text_x = center_x - extents.width() / 2.0; - - self.context.move_to(text_x, text_y); + let te = self.context.text_extents(&self.password_display).unwrap(); self.context - .show_text(&self.password_display) - .expect("Failed to draw password"); + .move_to(center_x - te.width() / 2.0, center_y + radius / 1.1); + self.context.show_text(&self.password_display).unwrap(); + } + + fn draw_caps_lock_indicator(&self) { + let center_x = self.width as f64 / 2.0; + let center_y = self.height as f64 / 2.0; + let radius = self.config.indicator_radius as f64; + self.context.new_path(); + self.context.set_font_size(12.0); + self.context.set_source_rgba(1.0, 0.5, 0.0, self.fade_alpha); + let text = "CAPS LOCK"; + let te = self.context.text_extents(text).unwrap(); + self.context + .move_to(center_x - te.width() / 2.0, center_y + radius / 1.1 + 20.0); + self.context.show_text(text).unwrap(); } - /// Draw wrong password feedback (red flash) fn draw_wrong_password_feedback(&self) { let center_x = self.width as f64 / 2.0; let center_y = self.height as f64 / 2.0; let radius = self.config.indicator_radius as f64; - - // Calculate flash intensity based on time + let thickness = self.config.indicator_thickness as f64; let intensity = if let Some(start) = self.wrong_password_start { let elapsed = start.elapsed(); let duration = std::time::Duration::from_millis(500); @@ -320,26 +305,24 @@ impl Renderer { }; if intensity > 0.0 { + self.context.new_path(); self.context .set_source_rgba(1.0, 0.0, 0.0, intensity * self.fade_alpha); + self.context.set_line_width(thickness + 2.0); self.context .arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI); - self.context - .fill() - .expect("Failed to draw wrong password feedback"); + self.context.stroke().unwrap(); } } - /// Draw key highlight feedback (green flash) fn draw_key_highlight_feedback(&self) { let center_x = self.width as f64 / 2.0; let center_y = self.height as f64 / 2.0; let radius = self.config.indicator_radius as f64; - - // Calculate flash intensity based on time + let thickness = self.config.indicator_thickness as f64; let intensity = if let Some(start) = self.key_highlight_start { let elapsed = start.elapsed(); - let duration = std::time::Duration::from_millis(200); + let duration = std::time::Duration::from_millis(300); if elapsed < duration { 1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64()) } else { @@ -350,34 +333,36 @@ impl Renderer { }; if intensity > 0.0 { - let key_hl_color = self.tuple_to_color(self.config.key_hl_color); - self.context.set_source_rgba( - key_hl_color.r as f64 / 255.0, - key_hl_color.g as f64 / 255.0, - key_hl_color.b as f64 / 255.0, - key_hl_color.a as f64 / 255.0 * intensity * self.fade_alpha, + let (r, g, b, a) = self.config.key_hl_color; + self.context + .set_source_rgba(r, g, b, a * intensity * self.fade_alpha); + self.context.set_line_width(thickness + 1.5); + + // Draw ONLY ONE segment that rotates based on password length + let global_offset = (self.password_display.len() as f64 * 45.0).to_radians(); + self.context.new_path(); + let actual_start = global_offset + self.key_highlight_angle; + self.context.arc( + center_x, + center_y, + radius, + actual_start, + actual_start + (40.0_f64).to_radians(), ); - self.context - .arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI); - self.context - .fill() - .expect("Failed to draw key highlight feedback"); + self.context.stroke().unwrap(); } } - /// Update feedback timers and reset expired feedback fn update_feedback_timers(&mut self) { - // Check wrong password feedback timeout + self.update_uptime(); if let Some(start) = self.wrong_password_start { if start.elapsed() > std::time::Duration::from_millis(500) { self.wrong_password_shown = false; self.wrong_password_start = None; } } - - // Check key highlight feedback timeout if let Some(start) = self.key_highlight_start { - if start.elapsed() > std::time::Duration::from_millis(200) { + if start.elapsed() > std::time::Duration::from_millis(300) { self.key_highlight_shown = false; self.key_highlight_start = None; } diff --git a/src/screenshot.rs b/src/screenshot.rs index 25dd4aa..6f23d72 100644 --- a/src/screenshot.rs +++ b/src/screenshot.rs @@ -28,11 +28,6 @@ impl Screenshot { Self { surface } } - /// Get a reference to the underlying surface. - pub fn surface(&self) -> &ImageSurface { - &self.surface - } - /// Consume the screenshot and return the underlying Cairo surface. pub fn into_inner(self) -> ImageSurface { self.surface @@ -171,11 +166,6 @@ impl ScreenshotManager { Ok(Self { manager }) } - /// Returns `true` if the wlr-screencopy protocol is available. - pub fn is_available(&self) -> bool { - self.manager.is_some() - } - /// Initiate a screencopy operation for the given output. /// /// This method sends a screencopy request and returns the frame object. diff --git a/src/timer.rs b/src/timer.rs index 764549e..8b13789 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -1,26 +1 @@ -use std::time::Duration; -pub struct FadeTimer { - duration: Duration, - start_time: std::time::Instant, -} - -impl FadeTimer { - pub fn new(duration: Duration) -> Self { - Self { - duration, - start_time: std::time::Instant::now(), - } - } - - pub fn update(&mut self) -> bool { - let elapsed = self.start_time.elapsed(); - let progress = elapsed.as_secs_f64() / self.duration.as_secs_f64(); - progress >= 1.0 - } - - pub fn current_alpha(&self) -> f64 { - let elapsed = self.start_time.elapsed(); - (elapsed.as_secs_f64() / self.duration.as_secs_f64()).min(1.0) - } -} diff --git a/src/util.rs b/src/util.rs index 0b66ea8..470fab3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -38,23 +38,3 @@ pub fn parse_vignette_effect(s: &str) -> Result<(f32, f32), String> { let factor = parts[1].parse().map_err(|_| "Invalid factor")?; Ok((base, factor)) } - -/// Convert hex color string to RGBA color struct -pub fn hex_to_rgba(hex: &str) -> Color { - let (r, g, b, a) = parse_hex_color(hex).unwrap_or((0.0, 0.0, 0.0, 1.0)); - Color { - r: (r * 255.0) as u8, - g: (g * 255.0) as u8, - b: (b * 255.0) as u8, - a: (a * 255.0) as u8, - } -} - -/// RGBA color struct -#[derive(Debug, Clone, Copy)] -pub struct Color { - pub r: u8, - pub g: u8, - pub b: u8, - pub a: u8, -}