Initial commit of an attempt of making my own wayland locker like swaylock

This commit is contained in:
2026-03-05 10:45:41 +01:00
commit a33488adc6
13 changed files with 4194 additions and 0 deletions
+66
View File
@@ -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");
}
}
+86
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
}
}
+99
View File
@@ -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
}
}
+26
View File
@@ -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
View File
@@ -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,
}