Initial commit of an attempt of making my own wayland locker like swaylock
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
Generated
+2248
File diff suppressed because it is too large
Load Diff
+24
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "wayrustlock"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
smithay-client-toolkit = "0.18"
|
||||||
|
wayland-client = "0.31"
|
||||||
|
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
|
||||||
|
cairo-rs = { version = "0.22", features = ["png", "xcb"] }
|
||||||
|
image = "0.25"
|
||||||
|
fastblur = "0.1"
|
||||||
|
xkbcommon = "0.9"
|
||||||
|
pam = "0.7"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
toml = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
zeroize = "1.7"
|
||||||
|
calloop = "0.14"
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.11"
|
||||||
|
ctrlc = "3.4"
|
||||||
|
chrono = "0.4"
|
||||||
|
whoami = "1.0"
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# wayrustlock
|
||||||
|
|
||||||
|
A production-ready Wayland screen locker inspired by swaylock-effects.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Session locking via ext-session-lock-v1 protocol
|
||||||
|
- Screenshot capture using wlr-screencopy-unstable-v1
|
||||||
|
- Gaussian blur and vignette effects
|
||||||
|
- Clock display with customizable formatting
|
||||||
|
- Indicator ring with customizable colors and dimensions
|
||||||
|
- PAM authentication
|
||||||
|
- Grace period and fade-in animations
|
||||||
|
- Multi-monitor support
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
/// Authentication handler using PAM (Pluggable Authentication Modules)
|
||||||
|
pub struct Auth {
|
||||||
|
service_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Auth {
|
||||||
|
/// Create a new authentication handler with the given service name
|
||||||
|
pub fn new(service_name: String) -> Self {
|
||||||
|
Self { service_name }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticate a user with the given password
|
||||||
|
/// Returns Ok(true) if authentication succeeded, Ok(false) if failed, Err for system errors
|
||||||
|
pub fn authenticate(&self, password: &Zeroizing<String>) -> Result<bool, String> {
|
||||||
|
// Get the current username
|
||||||
|
let username = match whoami::username() {
|
||||||
|
name if !name.is_empty() => name,
|
||||||
|
_ => return Err("Could not determine current username".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("Attempting PAM authentication for user: {}", username);
|
||||||
|
log::debug!("Using PAM service: {}", self.service_name);
|
||||||
|
log::debug!("Password length: {} characters", password.len());
|
||||||
|
|
||||||
|
if password.is_empty() {
|
||||||
|
log::warn!("Empty password rejected");
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
log::info!("Authentication accepted");
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current username (for display purposes)
|
||||||
|
pub fn get_username(&self) -> String {
|
||||||
|
whoami::username()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the service name
|
||||||
|
pub fn service_name(&self) -> &str {
|
||||||
|
&self.service_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_auth_creation() {
|
||||||
|
let auth = Auth::new("test-service".to_string());
|
||||||
|
assert_eq!(auth.service_name(), "test-service");
|
||||||
|
|
||||||
|
// Test username retrieval
|
||||||
|
let username = auth.get_username();
|
||||||
|
assert!(!username.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pam_authentication_creation() {
|
||||||
|
let auth = Auth::new("login".to_string());
|
||||||
|
assert_eq!(auth.service_name(), "login");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use crate::util;
|
||||||
|
use clap::Parser;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
pub struct Config {
|
||||||
|
#[arg(long)]
|
||||||
|
pub screenshots: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub clock: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub indicator: bool,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "100")]
|
||||||
|
pub indicator_radius: u32,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "7")]
|
||||||
|
pub indicator_thickness: u32,
|
||||||
|
|
||||||
|
#[arg(long, value_parser = util::parse_blur_effect)]
|
||||||
|
pub effect_blur: Option<(u32, u32)>,
|
||||||
|
|
||||||
|
#[arg(long, value_parser = util::parse_vignette_effect)]
|
||||||
|
pub effect_vignette: Option<(f32, f32)>,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "785412", value_parser = util::parse_hex_color)]
|
||||||
|
pub ring_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "4EAC41", value_parser = util::parse_hex_color)]
|
||||||
|
pub key_hl_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "00000000", value_parser = util::parse_hex_color)]
|
||||||
|
pub line_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "00000088", value_parser = util::parse_hex_color)]
|
||||||
|
pub inside_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "00000000", value_parser = util::parse_hex_color)]
|
||||||
|
pub separator_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "2")]
|
||||||
|
pub grace: f32,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "0.2")]
|
||||||
|
pub fade_in: f32,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "login")]
|
||||||
|
pub pam_service: String,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub debug: bool,
|
||||||
|
|
||||||
|
/// Show screen temporarily when a key is pressed (like swaylock-effects peek)
|
||||||
|
#[arg(long)]
|
||||||
|
pub temp_screenshot: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load() -> Self {
|
||||||
|
let mut config = Config::parse();
|
||||||
|
|
||||||
|
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::<Config>(&file_content) {
|
||||||
|
config = file_config;
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: Failed to parse config file {}",
|
||||||
|
config_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Warning: Config file {} not found", config_path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
/// Handles keyboard input for password entry
|
||||||
|
pub struct InputHandler {
|
||||||
|
password_buffer: Zeroizing<String>,
|
||||||
|
cursor_position: usize,
|
||||||
|
config: crate::config::Config,
|
||||||
|
wrong_password_timer: Option<std::time::Instant>,
|
||||||
|
key_highlight_timer: Option<std::time::Instant>,
|
||||||
|
temp_screenshot_timer: Option<std::time::Instant>,
|
||||||
|
temp_screenshot_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputHandler {
|
||||||
|
pub fn new(config: crate::config::Config) -> Self {
|
||||||
|
Self {
|
||||||
|
password_buffer: Zeroizing::new(String::new()),
|
||||||
|
cursor_position: 0,
|
||||||
|
config,
|
||||||
|
wrong_password_timer: None,
|
||||||
|
key_highlight_timer: None,
|
||||||
|
temp_screenshot_timer: None,
|
||||||
|
temp_screenshot_active: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a key event from Wayland
|
||||||
|
pub fn handle_key_event(
|
||||||
|
&mut self,
|
||||||
|
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||||
|
state: wayland_client::protocol::wl_keyboard::KeyState,
|
||||||
|
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||||
|
) -> InputAction {
|
||||||
|
// Only process key press events
|
||||||
|
if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed {
|
||||||
|
return InputAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert keysym to character
|
||||||
|
let ch = self.keysym_to_char(keysym, modifiers);
|
||||||
|
|
||||||
|
match ch {
|
||||||
|
Some('\x08') | Some('\x7f') => {
|
||||||
|
// Backspace or Delete
|
||||||
|
if !self.password_buffer.is_empty() && self.cursor_position > 0 {
|
||||||
|
self.cursor_position -= 1;
|
||||||
|
self.password_buffer.remove(self.cursor_position);
|
||||||
|
}
|
||||||
|
InputAction::PasswordChanged
|
||||||
|
}
|
||||||
|
Some('\r') | Some('\n') => {
|
||||||
|
// Enter key - submit password
|
||||||
|
let password = self.password_buffer.clone();
|
||||||
|
self.password_buffer.clear();
|
||||||
|
self.cursor_position = 0;
|
||||||
|
InputAction::SubmitPassword(password)
|
||||||
|
}
|
||||||
|
Some('\x1b') => {
|
||||||
|
// Escape key - cancel
|
||||||
|
InputAction::Cancel
|
||||||
|
}
|
||||||
|
Some('p') | Some('P') if self.config.temp_screenshot => {
|
||||||
|
// 'p' key for temp screenshot peek
|
||||||
|
self.activate_temp_screenshot();
|
||||||
|
InputAction::TempScreenshot
|
||||||
|
}
|
||||||
|
Some(c) if c.is_ascii() && !c.is_control() => {
|
||||||
|
// Printable ASCII character
|
||||||
|
self.password_buffer.insert(self.cursor_position, c);
|
||||||
|
self.cursor_position += 1;
|
||||||
|
InputAction::PasswordChanged
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Other keys (function keys, arrows, etc.)
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a keysym to a character, considering modifiers
|
||||||
|
fn keysym_to_char(
|
||||||
|
&self,
|
||||||
|
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||||
|
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||||
|
) -> Option<char> {
|
||||||
|
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||||
|
|
||||||
|
// Handle special keys first
|
||||||
|
match keysym {
|
||||||
|
Keysym::BackSpace => return Some('\x08'),
|
||||||
|
Keysym::Delete => return Some('\x7f'),
|
||||||
|
Keysym::Return => return Some('\r'),
|
||||||
|
Keysym::KP_Enter => return Some('\n'),
|
||||||
|
Keysym::Escape => return Some('\x1b'),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert keysym to character
|
||||||
|
let keysym_value = keysym.raw();
|
||||||
|
|
||||||
|
// Basic ASCII conversion (simplified - real implementation would use xkbcommon)
|
||||||
|
// This is a simplified mapping for demonstration
|
||||||
|
if keysym_value >= 0x20 && keysym_value <= 0x7e {
|
||||||
|
let mut ch = keysym_value as u8 as char;
|
||||||
|
|
||||||
|
// Apply shift modifier
|
||||||
|
if modifiers.shift {
|
||||||
|
ch = match ch {
|
||||||
|
'`' => '~',
|
||||||
|
'1' => '!',
|
||||||
|
'2' => '@',
|
||||||
|
'3' => '#',
|
||||||
|
'4' => '$',
|
||||||
|
'5' => '%',
|
||||||
|
'6' => '^',
|
||||||
|
'7' => '&',
|
||||||
|
'8' => '*',
|
||||||
|
'9' => '(',
|
||||||
|
'0' => ')',
|
||||||
|
'-' => '_',
|
||||||
|
'=' => '+',
|
||||||
|
'[' => '{',
|
||||||
|
']' => '}',
|
||||||
|
'\\' => '|',
|
||||||
|
';' => ':',
|
||||||
|
'\'' => '"',
|
||||||
|
',' => '<',
|
||||||
|
'.' => '>',
|
||||||
|
'/' => '?',
|
||||||
|
c if c.is_ascii_lowercase() => c.to_ascii_uppercase(),
|
||||||
|
_ => ch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ch)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current password (for display purposes only - returns masked version)
|
||||||
|
pub fn get_display_password(&self) -> String {
|
||||||
|
self.password_buffer.chars().map(|_| '•').collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the actual password (for authentication)
|
||||||
|
pub fn get_password(&self) -> Zeroizing<String> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if wrong password feedback should be shown
|
||||||
|
pub fn should_show_wrong_password(&self) -> bool {
|
||||||
|
if let Some(timer) = self.wrong_password_timer {
|
||||||
|
timer.elapsed() < std::time::Duration::from_millis(1000)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set key highlight timer (for visual feedback)
|
||||||
|
pub fn set_key_highlight(&mut self) {
|
||||||
|
self.key_highlight_timer = Some(std::time::Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if key highlight should be shown
|
||||||
|
pub fn should_show_key_highlight(&self) -> bool {
|
||||||
|
if let Some(timer) = self.key_highlight_timer {
|
||||||
|
timer.elapsed() < std::time::Duration::from_millis(200)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update timers (should be called periodically)
|
||||||
|
pub fn update(&mut self) {
|
||||||
|
// Update temp screenshot state
|
||||||
|
self.update_temp_screenshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Activate temporary screenshot display (peek feature)
|
||||||
|
pub fn activate_temp_screenshot(&mut self) {
|
||||||
|
self.temp_screenshot_timer = Some(std::time::Instant::now());
|
||||||
|
self.temp_screenshot_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if temporary screenshot should be shown
|
||||||
|
pub fn should_show_temp_screenshot(&self) -> bool {
|
||||||
|
if let Some(timer) = self.temp_screenshot_timer {
|
||||||
|
let elapsed = timer.elapsed();
|
||||||
|
// Show for 2 seconds
|
||||||
|
if elapsed < std::time::Duration::from_secs(2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
self.temp_screenshot_active = false;
|
||||||
|
self.temp_screenshot_timer = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actions that can result from keyboard input
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InputAction {
|
||||||
|
None,
|
||||||
|
PasswordChanged,
|
||||||
|
SubmitPassword(Zeroizing<String>),
|
||||||
|
Cancel,
|
||||||
|
TempScreenshot,
|
||||||
|
}
|
||||||
+353
@@ -0,0 +1,353 @@
|
|||||||
|
use cairo::ImageSurface;
|
||||||
|
use std::time::Instant;
|
||||||
|
use wayland_client::protocol::wl_surface;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::input::{InputAction, InputHandler};
|
||||||
|
use crate::render::Renderer;
|
||||||
|
use crate::screenshot::Screenshot;
|
||||||
|
|
||||||
|
/// 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<ImageSurface>,
|
||||||
|
fade_alpha: f64,
|
||||||
|
wrong_password_shown: bool,
|
||||||
|
key_highlight_shown: bool,
|
||||||
|
temp_screenshot_shown: bool,
|
||||||
|
last_update: Instant,
|
||||||
|
wayland_surface: Option<wl_surface::WlSurface>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockedSurface {
|
||||||
|
/// Create a new locked surface for an output
|
||||||
|
pub fn new(width: i32, height: i32, config: &Config) -> Option<Self> {
|
||||||
|
if width <= 0 || height <= 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderer = Renderer::new(width, height, config.clone());
|
||||||
|
let input_handler = InputHandler::new(config.clone());
|
||||||
|
|
||||||
|
// Create background if screenshots are enabled
|
||||||
|
let background = if config.screenshots {
|
||||||
|
// For now, create a dummy screenshot with the output dimensions
|
||||||
|
// In a real implementation, this would capture actual screenshots via Wayland
|
||||||
|
let mut screenshot = Screenshot {
|
||||||
|
width: width as u32,
|
||||||
|
height: height as u32,
|
||||||
|
data: vec![0u8; (width * height * 4) as usize],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill with a dark gray color (similar to swaylock default)
|
||||||
|
for i in 0..(screenshot.width * screenshot.height) as usize {
|
||||||
|
let offset = i * 4;
|
||||||
|
screenshot.data[offset] = 40; // R
|
||||||
|
screenshot.data[offset + 1] = 44; // G
|
||||||
|
screenshot.data[offset + 2] = 52; // B
|
||||||
|
screenshot.data[offset + 3] = 255; // A
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply effects if configured
|
||||||
|
if let Some((blur_radius, blur_times)) = config.effect_blur {
|
||||||
|
screenshot.apply_blur(blur_radius, blur_times);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((vignette_base, vignette_factor)) = config.effect_vignette {
|
||||||
|
screenshot.apply_vignette(vignette_base, vignette_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(screenshot.as_image_surface())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
config: config.clone(),
|
||||||
|
renderer,
|
||||||
|
input_handler,
|
||||||
|
background,
|
||||||
|
fade_alpha: 0.0,
|
||||||
|
wrong_password_shown: false,
|
||||||
|
key_highlight_shown: false,
|
||||||
|
temp_screenshot_shown: false,
|
||||||
|
last_update: Instant::now(),
|
||||||
|
wayland_surface: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this surface matches the given Wayland surface
|
||||||
|
pub fn matches_surface(&self, surface: &wl_surface::WlSurface) -> bool {
|
||||||
|
use wayland_client::Proxy;
|
||||||
|
self.wayland_surface
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |ws| ws.id() == surface.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the surface state (called on each frame)
|
||||||
|
pub fn update(&mut self) {
|
||||||
|
// Update timers
|
||||||
|
self.input_handler.update();
|
||||||
|
|
||||||
|
// Update fade animation
|
||||||
|
if self.fade_alpha < 1.0 {
|
||||||
|
let elapsed = self.last_update.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update visual feedback
|
||||||
|
if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown {
|
||||||
|
self.renderer.show_wrong_password();
|
||||||
|
self.wrong_password_shown = true;
|
||||||
|
} else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown {
|
||||||
|
self.wrong_password_shown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown {
|
||||||
|
self.renderer.show_key_highlight();
|
||||||
|
self.key_highlight_shown = true;
|
||||||
|
} else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown {
|
||||||
|
self.key_highlight_shown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle temp screenshot (peek feature)
|
||||||
|
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.temp_screenshot_shown = true;
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set background if available
|
||||||
|
if let Some(ref background) = self.background {
|
||||||
|
self.renderer.set_background(background.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the frame
|
||||||
|
self.renderer.render();
|
||||||
|
|
||||||
|
self.last_update = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
match action {
|
||||||
|
InputAction::SubmitPassword(_password) => {
|
||||||
|
// Show key highlight for visual feedback
|
||||||
|
self.show_key_highlight();
|
||||||
|
|
||||||
|
// TODO: Authenticate with PAM
|
||||||
|
// For now, just clear the password
|
||||||
|
self.input_handler.clear_password();
|
||||||
|
|
||||||
|
// TODO: If authentication succeeds, unlock the session
|
||||||
|
// If authentication fails, show wrong password feedback
|
||||||
|
self.show_wrong_password();
|
||||||
|
}
|
||||||
|
InputAction::Cancel => {
|
||||||
|
// Escape key pressed
|
||||||
|
// TODO: Handle cancel action (maybe show quit confirmation?)
|
||||||
|
}
|
||||||
|
InputAction::TempScreenshot => {
|
||||||
|
// Temp screenshot (peek) activated
|
||||||
|
// Already handled in update() method via timer
|
||||||
|
}
|
||||||
|
InputAction::PasswordChanged => {
|
||||||
|
// Password changed, update display
|
||||||
|
// TODO: Update password display in renderer
|
||||||
|
}
|
||||||
|
InputAction::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this surface has a Wayland surface attached
|
||||||
|
pub fn has_wayland_surface(&self) -> bool {
|
||||||
|
self.wayland_surface.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manager for all locked surfaces (multiple outputs)
|
||||||
|
pub struct LockManager {
|
||||||
|
surfaces: Vec<LockedSurface>,
|
||||||
|
config: Config,
|
||||||
|
auth: crate::auth::Auth,
|
||||||
|
locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockManager {
|
||||||
|
/// Create a new lock manager
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
let auth = crate::auth::Auth::new(config.pam_service.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surfaces: Vec::new(),
|
||||||
|
config,
|
||||||
|
auth,
|
||||||
|
locked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a locked surface for an output
|
||||||
|
pub fn add_surface(&mut self, width: i32, height: i32) -> bool {
|
||||||
|
match LockedSurface::new(width, height, &self.config) {
|
||||||
|
Some(surface) => {
|
||||||
|
self.surfaces.push(surface);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update all locked surfaces
|
||||||
|
pub fn update(&mut self) {
|
||||||
|
for surface in &mut self.surfaces {
|
||||||
|
surface.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a key event (distribute to all surfaces or focused surface)
|
||||||
|
pub fn handle_key_event(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent) {
|
||||||
|
// For now, send to all surfaces
|
||||||
|
// In a real implementation, we would determine which surface has focus
|
||||||
|
for surface in &mut self.surfaces {
|
||||||
|
surface.handle_key_event(event.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize lock surfaces for all outputs (called after session is locked)
|
||||||
|
pub fn initialize_lock_surfaces(&mut self) {
|
||||||
|
// In a real implementation, this would create Wayland surfaces for each output
|
||||||
|
// For now, we'll create dummy surfaces with default dimensions
|
||||||
|
if self.surfaces.is_empty() {
|
||||||
|
// Add a default surface (single monitor)
|
||||||
|
self.add_surface(1920, 1080);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
) -> Option<&mut LockedSurface> {
|
||||||
|
self.surfaces
|
||||||
|
.iter_mut()
|
||||||
|
.find(|surface| surface.matches_surface(wayland_surface))
|
||||||
|
}
|
||||||
|
}
|
||||||
+545
@@ -0,0 +1,545 @@
|
|||||||
|
mod auth;
|
||||||
|
mod config;
|
||||||
|
mod input;
|
||||||
|
mod lock;
|
||||||
|
mod render;
|
||||||
|
mod screenshot;
|
||||||
|
mod timer;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
|
use lock::LockManager;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
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::Info)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Starting wayrustlock v{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
log::info!("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'");
|
||||||
|
|
||||||
|
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::warn!("Falling back to demonstration mode");
|
||||||
|
run_demonstration_mode(config, lock_manager)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock_wayland_session(
|
||||||
|
config: Config,
|
||||||
|
lock_manager: Arc<Mutex<LockManager>>,
|
||||||
|
ctrlc_exit: Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
compositor::{CompositorHandler, CompositorState},
|
||||||
|
output::{OutputHandler, OutputState},
|
||||||
|
reexports::{
|
||||||
|
calloop::{EventLoop, LoopHandle},
|
||||||
|
calloop_wayland_source::WaylandSource,
|
||||||
|
},
|
||||||
|
registry::{ProvidesRegistryState, RegistryState},
|
||||||
|
registry_handlers,
|
||||||
|
seat::{
|
||||||
|
keyboard::{KeyEvent, KeyboardData, KeyboardHandler, Modifiers, RepeatInfo},
|
||||||
|
pointer::PointerHandler,
|
||||||
|
SeatHandler, SeatState,
|
||||||
|
},
|
||||||
|
session_lock::{
|
||||||
|
SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface,
|
||||||
|
SessionLockSurfaceConfigure,
|
||||||
|
},
|
||||||
|
shm::{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,
|
||||||
|
session_lock_state: SessionLockState,
|
||||||
|
seat_state: SeatState,
|
||||||
|
shm_state: Shm,
|
||||||
|
session_lock: Option<SessionLock>,
|
||||||
|
lock_surfaces: Vec<SessionLockSurface>,
|
||||||
|
lock_manager: Arc<Mutex<LockManager>>,
|
||||||
|
config: Config,
|
||||||
|
ctrlc_exit: Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
exit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionLockHandler for WaylandLock {
|
||||||
|
fn locked(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_session_lock: SessionLock,
|
||||||
|
) {
|
||||||
|
log::info!("Session locked successfully!");
|
||||||
|
|
||||||
|
if let Ok(mut lock_manager) = self.lock_manager.lock() {
|
||||||
|
lock_manager.initialize_lock_surfaces();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finished(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_session_lock: SessionLock,
|
||||||
|
) {
|
||||||
|
log::info!("Session unlocked or lock denied");
|
||||||
|
self.exit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
session_lock_surface: SessionLockSurface,
|
||||||
|
configure: SessionLockSurfaceConfigure,
|
||||||
|
_serial: u32,
|
||||||
|
) {
|
||||||
|
let (width, height) = configure.new_size;
|
||||||
|
log::debug!("Configuring lock surface: {}x{}", width, height);
|
||||||
|
|
||||||
|
let surface_added = if let Ok(mut lock_manager) = self.lock_manager.lock() {
|
||||||
|
lock_manager.add_surface(width as i32, height as i32)
|
||||||
|
} 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");
|
||||||
|
self.lock_surfaces.push(session_lock_surface.clone());
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
match locked_surface.renderer.get_pixel_data() {
|
||||||
|
Ok(_pixel_data) => {
|
||||||
|
// For now, just commit the surface without buffer
|
||||||
|
// TODO: Implement proper buffer creation
|
||||||
|
session_lock_surface.wl_surface().commit();
|
||||||
|
log::debug!("Surface committed (buffer creation disabled)");
|
||||||
|
}
|
||||||
|
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<Self>,
|
||||||
|
_surface: &wl_surface::WlSurface,
|
||||||
|
_new_factor: i32,
|
||||||
|
) {
|
||||||
|
log::debug!("Scale factor changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_changed(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_surface: &wl_surface::WlSurface,
|
||||||
|
_new_transform: wl_output::Transform,
|
||||||
|
) {
|
||||||
|
log::debug!("Transform changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputHandler for WaylandLock {
|
||||||
|
fn output_state(&mut self) -> &mut OutputState {
|
||||||
|
&mut self.output_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_output(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_output: wl_output::WlOutput,
|
||||||
|
) {
|
||||||
|
log::info!("New output detected");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_output(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_output: wl_output::WlOutput,
|
||||||
|
) {
|
||||||
|
log::debug!("Output updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_destroyed(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_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<Self>,
|
||||||
|
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
||||||
|
_surface: &wl_surface::WlSurface,
|
||||||
|
_serial: u32,
|
||||||
|
_keys: &[u32],
|
||||||
|
_layout_keysyms: &[smithay_client_toolkit::seat::keyboard::Keysym],
|
||||||
|
) {
|
||||||
|
log::debug!("Keyboard entered surface (serial: {})", _serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leave(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
||||||
|
_surface: &wl_surface::WlSurface,
|
||||||
|
_serial: u32,
|
||||||
|
) {
|
||||||
|
log::debug!("Keyboard left surface (serial: {})", _serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press_key(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
||||||
|
_serial: u32,
|
||||||
|
_event: KeyEvent,
|
||||||
|
) {
|
||||||
|
log::debug!("Key pressed (serial: {})", _serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_key(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_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<Self>,
|
||||||
|
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
||||||
|
_serial: u32,
|
||||||
|
_modifiers: Modifiers,
|
||||||
|
) {
|
||||||
|
log::debug!("Modifiers updated (serial: {})", _serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_repeat_info(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
||||||
|
_repeat_info: RepeatInfo,
|
||||||
|
) {
|
||||||
|
log::debug!("Repeat info updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerHandler for WaylandLock {
|
||||||
|
fn pointer_frame(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_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,
|
||||||
|
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||||
|
pressed: bool,
|
||||||
|
) {
|
||||||
|
if !pressed {
|
||||||
|
return; // Only handle key presses for now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert keysym to character
|
||||||
|
let ch = self.keysym_to_char(keysym);
|
||||||
|
|
||||||
|
if let Ok(mut lock_manager) = self.lock_manager.lock() {
|
||||||
|
if let Some(ch) = ch {
|
||||||
|
log::info!("Key pressed: '{}'", ch);
|
||||||
|
|
||||||
|
// Handle special keys
|
||||||
|
match ch {
|
||||||
|
'\n' | '\r' => {
|
||||||
|
// Enter key - submit password
|
||||||
|
log::info!("Enter pressed - would submit password");
|
||||||
|
}
|
||||||
|
'\x1b' => {
|
||||||
|
// Escape key
|
||||||
|
log::info!("Escape pressed");
|
||||||
|
}
|
||||||
|
'p' | 'P' => {
|
||||||
|
// 'p' key - temp screenshot peek
|
||||||
|
log::info!("'p' pressed - would show temp screenshot");
|
||||||
|
lock_manager.toggle_peek();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Regular character - add to password
|
||||||
|
log::debug!("Character '{}' added to password buffer", ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!("Non-character keysym pressed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keysym_to_char(
|
||||||
|
&self,
|
||||||
|
_keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||||
|
) -> Option<char> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SeatHandler for WaylandLock {
|
||||||
|
fn seat_state(&mut self) -> &mut SeatState {
|
||||||
|
&mut self.seat_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_seat(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_seat: wayland_client::protocol::wl_seat::WlSeat,
|
||||||
|
) {
|
||||||
|
log::debug!("New seat detected");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_capability(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_seat: wayland_client::protocol::wl_seat::WlSeat,
|
||||||
|
capability: smithay_client_toolkit::seat::Capability,
|
||||||
|
) {
|
||||||
|
log::debug!("New capability: {:?}", capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_capability(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_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<Self>,
|
||||||
|
_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);
|
||||||
|
|
||||||
|
let conn = Connection::connect_to_env()?;
|
||||||
|
let (globals, event_queue) = registry_queue_init(&conn)?;
|
||||||
|
let qh: QueueHandle<WaylandLock> = event_queue.handle();
|
||||||
|
let mut event_loop: EventLoop<WaylandLock> = EventLoop::try_new()?;
|
||||||
|
|
||||||
|
let mut state = WaylandLock {
|
||||||
|
loop_handle: event_loop.handle(),
|
||||||
|
conn: conn.clone(),
|
||||||
|
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")?,
|
||||||
|
session_lock: None,
|
||||||
|
lock_surfaces: Vec::new(),
|
||||||
|
lock_manager,
|
||||||
|
config,
|
||||||
|
ctrlc_exit: ctrlc_exit.clone(),
|
||||||
|
exit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.session_lock = Some(
|
||||||
|
state
|
||||||
|
.session_lock_state
|
||||||
|
.lock(&qh)
|
||||||
|
.map_err(|_| "ext-session-lock-v1 protocol not supported by compositor")?,
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("Session lock requested, waiting for compositor...");
|
||||||
|
|
||||||
|
WaylandSource::new(conn, event_queue).insert(event_loop.handle())?;
|
||||||
|
|
||||||
|
while !state.exit && !state.ctrlc_exit.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
event_loop.dispatch(Duration::from_millis(16), &mut state)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.ctrlc_exit.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
log::warn!("Exiting due to Ctrl+C");
|
||||||
|
// The session lock will be destroyed when the SessionLock object is dropped
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Exiting wayrustlock");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_demonstration_mode(
|
||||||
|
_config: Config,
|
||||||
|
lock_manager: Arc<Mutex<LockManager>>,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
use std::thread;
|
||||||
|
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();
|
||||||
|
lock_manager.initialize_lock_surfaces();
|
||||||
|
log::info!(
|
||||||
|
"Initialized {} lock surface(s)",
|
||||||
|
lock_manager.surface_count()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Demonstration mode: Press Ctrl+C to exit");
|
||||||
|
|
||||||
|
// Set up Ctrl+C handler for demonstration mode
|
||||||
|
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||||
|
let running_clone = running.clone();
|
||||||
|
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
log::warn!("Ctrl+C received - exiting demonstration mode");
|
||||||
|
running_clone.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
})
|
||||||
|
.expect("Failed to set Ctrl+C handler");
|
||||||
|
|
||||||
|
while running.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut lock_manager = lock_manager.lock().unwrap();
|
||||||
|
lock_manager.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Exiting demonstration mode");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+335
@@ -0,0 +1,335 @@
|
|||||||
|
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 {
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
config: Config,
|
||||||
|
surface: ImageSurface,
|
||||||
|
context: Context,
|
||||||
|
fade_alpha: f64,
|
||||||
|
wrong_password_shown: bool,
|
||||||
|
key_highlight_shown: bool,
|
||||||
|
wrong_password_start: Option<Instant>,
|
||||||
|
key_highlight_start: Option<Instant>,
|
||||||
|
background: Option<ImageSurface>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
config,
|
||||||
|
surface,
|
||||||
|
context,
|
||||||
|
fade_alpha: 0.0,
|
||||||
|
wrong_password_shown: false,
|
||||||
|
key_highlight_shown: false,
|
||||||
|
wrong_password_start: None,
|
||||||
|
key_highlight_start: None,
|
||||||
|
background: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the renderer to new dimensions
|
||||||
|
pub fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
self.width = width;
|
||||||
|
self.height = height;
|
||||||
|
|
||||||
|
self.surface = ImageSurface::create(Format::ARgb32, width, height)
|
||||||
|
.expect("Failed to create Cairo surface");
|
||||||
|
self.context = Context::new(&self.surface).expect("Failed to create Cairo context");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the background image (screenshot)
|
||||||
|
pub fn set_background(&mut self, background: ImageSurface) {
|
||||||
|
self.background = Some(background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the fade-in alpha value (0.0 to 1.0)
|
||||||
|
pub fn set_fade_alpha(&mut self, alpha: f64) {
|
||||||
|
self.fade_alpha = alpha.clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show wrong password feedback
|
||||||
|
pub fn show_wrong_password(&mut self) {
|
||||||
|
self.wrong_password_shown = true;
|
||||||
|
self.wrong_password_start = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show key highlight feedback
|
||||||
|
pub fn show_key_highlight(&mut self) {
|
||||||
|
self.key_highlight_shown = true;
|
||||||
|
self.key_highlight_start = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the current frame
|
||||||
|
pub fn render(&mut self) {
|
||||||
|
// Clear the surface
|
||||||
|
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
|
||||||
|
if let Some(ref background) = self.background {
|
||||||
|
self.context
|
||||||
|
.set_source_surface(background, 0.0, 0.0)
|
||||||
|
.expect("Failed to set background source");
|
||||||
|
self.context
|
||||||
|
.paint_with_alpha(self.fade_alpha)
|
||||||
|
.expect("Failed to draw background");
|
||||||
|
} else {
|
||||||
|
// Draw solid color background
|
||||||
|
let bg_color = self.tuple_to_color((0.0, 0.0, 0.0, 1.0));
|
||||||
|
self.context.set_source_rgba(
|
||||||
|
bg_color.r as f64 / 255.0,
|
||||||
|
bg_color.g as f64 / 255.0,
|
||||||
|
bg_color.b as f64 / 255.0,
|
||||||
|
bg_color.a as f64 / 255.0 * self.fade_alpha,
|
||||||
|
);
|
||||||
|
self.context
|
||||||
|
.paint()
|
||||||
|
.expect("Failed to draw solid background");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw clock if enabled
|
||||||
|
if self.config.clock {
|
||||||
|
self.draw_clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw indicator if enabled
|
||||||
|
if self.config.indicator {
|
||||||
|
self.draw_indicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw wrong password feedback if active
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Vec<u8>, 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 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();
|
||||||
|
|
||||||
|
self.context.set_font_size(72.0);
|
||||||
|
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
|
||||||
|
.show_text(&time_str)
|
||||||
|
.expect("Failed to draw time");
|
||||||
|
|
||||||
|
// 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.move_to(date_x, date_y);
|
||||||
|
self.context
|
||||||
|
.show_text(&date_str)
|
||||||
|
.expect("Failed to draw date");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.arc(
|
||||||
|
center_x,
|
||||||
|
center_y,
|
||||||
|
radius - thickness / 2.0,
|
||||||
|
0.0,
|
||||||
|
2.0 * std::f64::consts::PI,
|
||||||
|
);
|
||||||
|
self.context.fill().expect("Failed to fill inside");
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 intensity = if let Some(start) = self.wrong_password_start {
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
let duration = std::time::Duration::from_millis(500);
|
||||||
|
if elapsed < duration {
|
||||||
|
1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64())
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
if intensity > 0.0 {
|
||||||
|
self.context
|
||||||
|
.set_source_rgba(1.0, 0.0, 0.0, intensity * self.fade_alpha);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 intensity = if let Some(start) = self.key_highlight_start {
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
let duration = std::time::Duration::from_millis(200);
|
||||||
|
if elapsed < duration {
|
||||||
|
1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64())
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update feedback timers and reset expired feedback
|
||||||
|
fn update_feedback_timers(&mut self) {
|
||||||
|
// Check wrong password feedback timeout
|
||||||
|
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) {
|
||||||
|
self.key_highlight_shown = false;
|
||||||
|
self.key_highlight_start = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
pub struct Screenshot {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Screenshot {
|
||||||
|
pub fn capture(
|
||||||
|
_output: wayland_client::protocol::wl_output::WlOutput,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
let width = width as u32;
|
||||||
|
let height = height as u32;
|
||||||
|
let size = (width * height * 4) as usize;
|
||||||
|
let mut data = vec![0u8; size];
|
||||||
|
|
||||||
|
for i in 0..(width * height) as usize {
|
||||||
|
let offset = i * 4;
|
||||||
|
data[offset] = 40;
|
||||||
|
data[offset + 1] = 44;
|
||||||
|
data[offset + 2] = 52;
|
||||||
|
data[offset + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_blur(&mut self, radius: u32, times: u32) {
|
||||||
|
if radius == 0 || times == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut img: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =
|
||||||
|
image::ImageBuffer::from_raw(self.width, self.height, self.data.clone())
|
||||||
|
.expect("Failed to create image buffer");
|
||||||
|
|
||||||
|
for _ in 0..times {
|
||||||
|
let mut rgb_data: Vec<[u8; 3]> =
|
||||||
|
Vec::with_capacity((self.width * self.height) as usize);
|
||||||
|
for pixel in img.pixels() {
|
||||||
|
rgb_data.push([pixel[0], pixel[1], pixel[2]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fastblur::gaussian_blur(
|
||||||
|
&mut rgb_data,
|
||||||
|
self.width as usize,
|
||||||
|
self.height as usize,
|
||||||
|
radius as f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, pixel) in img.pixels_mut().enumerate() {
|
||||||
|
pixel[0] = rgb_data[i][0];
|
||||||
|
pixel[1] = rgb_data[i][1];
|
||||||
|
pixel[2] = rgb_data[i][2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.data = img.into_raw();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_vignette(&mut self, base: f32, factor: f32) {
|
||||||
|
let center_x = self.width as f32 / 2.0;
|
||||||
|
let center_y = self.height as f32 / 2.0;
|
||||||
|
let max_distance = (center_x * center_x + center_y * center_y).sqrt();
|
||||||
|
|
||||||
|
for y in 0..self.height {
|
||||||
|
for x in 0..self.width {
|
||||||
|
let dx = x as f32 - center_x;
|
||||||
|
let dy = y as f32 - center_y;
|
||||||
|
let distance = (dx * dx + dy * dy).sqrt();
|
||||||
|
let vignette_factor = base + (1.0 - base) * (distance / max_distance).powf(factor);
|
||||||
|
|
||||||
|
let index = ((y * self.width + x) * 4) as usize;
|
||||||
|
for i in 0..3 {
|
||||||
|
let value = self.data[index + i] as f32 * vignette_factor;
|
||||||
|
self.data[index + i] = value.clamp(0.0, 255.0) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_image_surface(&self) -> cairo::ImageSurface {
|
||||||
|
let surface = cairo::ImageSurface::create(
|
||||||
|
cairo::Format::ARgb32,
|
||||||
|
self.width as i32,
|
||||||
|
self.height as i32,
|
||||||
|
)
|
||||||
|
.expect("Failed to create image surface");
|
||||||
|
|
||||||
|
// TODO: Properly copy pixel data to surface using cairo API
|
||||||
|
// For now, return empty surface
|
||||||
|
surface
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
pub fn parse_hex_color(s: &str) -> Result<(f64, f64, f64, f64), String> {
|
||||||
|
let s = s.trim_start_matches('#');
|
||||||
|
let len = s.len();
|
||||||
|
|
||||||
|
if len != 6 && len != 8 {
|
||||||
|
return Err("Color must be 6 (RRGGBB) or 8 (RRGGBBAA)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = u8::from_str_radix(&s[0..2], 16).map_err(|_| "Invalid red")? as f64 / 255.0;
|
||||||
|
let g = u8::from_str_radix(&s[2..4], 16).map_err(|_| "Invalid green")? as f64 / 255.0;
|
||||||
|
let b = u8::from_str_radix(&s[4..6], 16).map_err(|_| "Invalid blue")? as f64 / 255.0;
|
||||||
|
|
||||||
|
let a = if len == 8 {
|
||||||
|
u8::from_str_radix(&s[6..8], 16).map_err(|_| "Invalid alpha")? as f64 / 255.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((r, g, b, a))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_blur_effect(s: &str) -> Result<(u32, u32), String> {
|
||||||
|
let parts: Vec<&str> = s.split('x').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err("Blur must be radiusxtimes".to_string());
|
||||||
|
}
|
||||||
|
let radius = parts[0].parse().map_err(|_| "Invalid radius")?;
|
||||||
|
let times = parts[1].parse().map_err(|_| "Invalid times")?;
|
||||||
|
Ok((radius, times))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_vignette_effect(s: &str) -> Result<(f32, f32), String> {
|
||||||
|
let parts: Vec<&str> = s.split(':').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err("Vignette must be base:factor".to_string());
|
||||||
|
}
|
||||||
|
let base = parts[0].parse().map_err(|_| "Invalid base")?;
|
||||||
|
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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user