Compare commits

...

2 Commits

Author SHA1 Message Date
jory 76b48f7c70 First propper working version with actual pam 2026-03-05 14:19:08 +01:00
jory 4ed70fd858 First kinda working version 2026-03-05 12:40:53 +01:00
7 changed files with 379 additions and 124 deletions
+10 -3
View File
@@ -7,14 +7,21 @@ A production-ready Wayland screen locker inspired by swaylock-effects.
**This tool is under active development.** Screen lockers can cause system lockups if they malfunction.
**If the screen locker gets stuck:**
- Type password and press **Enter** to unlock (demo mode - any password works)
- Switch to another TTY: Press `Ctrl+Alt+F2`, login, then run `pkill -9 wayrustlock`
- From another terminal: `pkill -9 wayrustlock` or `killall wayrustlock`
- If screen is black/red: hard restart may be required
**Always test with a timeout first:**
**Debug logging:** Check `~/.wayrustlock.log` to see what's happening
**Test with timeout first:**
```bash
timeout 5 ./target/release/wayrustlock --indicator --clock
# If stuck, system will auto-unlock after 5 seconds
timeout 15 ./target/release/wayrustlock --indicator --clock
```
Then check the log file:
```bash
cat ~/.wayrustlock.log
```
## Features (Implemented vs Planned)
+5
View File
@@ -0,0 +1,5 @@
# PAM configuration for wayrustlock
# Install this file to /etc/pam.d/wayrustlock
# Use the standard login service authentication
auth include login
+5 -54
View File
@@ -3,63 +3,14 @@ use std::thread;
use log::{debug, error};
use pam_client::{Context, ErrorCode, Flag};
use secstr::SecVec;
use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop};
use users::get_current_username;
use zeroize::Zeroizing;
const SERVICE_NAME: &str = "wayrustlock";
pub struct PasswordBuffer(SecVec<u8>);
impl PasswordBuffer {
pub fn new() -> Self {
Self(SecVec::new(Vec::new()))
}
fn zeroize_string(mut data: String) {
use std::sync::atomic;
let default = u8::default();
for c in unsafe { data.as_bytes_mut() } {
unsafe { std::ptr::write_volatile(c, default) };
}
atomic::fence(atomic::Ordering::SeqCst);
atomic::compiler_fence(atomic::Ordering::SeqCst);
}
pub fn append(&mut self, data: String) {
let bytes = data.as_bytes();
let mut og_len = self.0.unsecure().len();
self.0.resize(og_len + bytes.len(), 0);
for b in bytes {
self.0.unsecure_mut()[og_len] = *b;
og_len += 1;
}
Self::zeroize_string(data);
}
pub fn backspace(&mut self) {
let og_len = self.0.unsecure().len();
if og_len != 0 {
self.0.resize(og_len - 1, 0);
}
}
pub fn unsecure(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.0.unsecure()) }
}
pub fn take(&mut self) -> Self {
let mut new_buffer = SecVec::new(Vec::new());
std::mem::swap(&mut self.0, &mut new_buffer);
Self(new_buffer)
}
}
pub struct LockConversation {
pub password: Option<PasswordBuffer>,
pub password: Option<Zeroizing<String>>,
}
impl pam_client::ConversationHandler for LockConversation {
@@ -71,7 +22,7 @@ impl pam_client::ConversationHandler for LockConversation {
fn prompt_echo_off(&mut self, _msg: &CStr) -> Result<CString, ErrorCode> {
if let Some(password) = self.password.take() {
CString::new(password.unsecure()).map_err(|_| ErrorCode::ABORT)
CString::new(password.as_str()).map_err(|_| ErrorCode::ABORT)
} else {
Err(ErrorCode::ABORT)
}
@@ -84,7 +35,7 @@ impl pam_client::ConversationHandler for LockConversation {
}
}
pub fn create_and_run_auth_loop() -> (channel::Sender<PasswordBuffer>, channel::Channel<bool>) {
pub fn create_and_run_auth_loop() -> (channel::Sender<Zeroizing<String>>, channel::Channel<bool>) {
struct AuthLoopState {
auth_res_send: channel::Sender<bool>,
main_closed: bool,
@@ -102,7 +53,7 @@ pub fn create_and_run_auth_loop() -> (channel::Sender<PasswordBuffer>, channel::
.expect("Failed to initialize PAM context");
debug!("Prepared to authenticate user '{}'", username);
let (auth_req_send, auth_req_recv) = channel::channel::<PasswordBuffer>();
let (auth_req_send, auth_req_recv) = channel::channel::<Zeroizing<String>>();
let (auth_res_send, auth_res_recv) = channel::channel::<bool>();
thread::spawn(move || {
+1 -1
View File
@@ -12,7 +12,7 @@ pub struct Config {
#[arg(long)]
pub clock: bool,
#[arg(long)]
#[arg(long, default_value = "true")]
pub indicator: bool,
#[arg(long, default_value = "100")]
+106 -29
View File
@@ -135,6 +135,9 @@ impl LockedSurface {
self.renderer.set_background(background.clone());
}
self.renderer
.set_password_display(self.input_handler.get_display_password());
// Render the frame
self.renderer.render();
@@ -171,7 +174,10 @@ impl LockedSurface {
}
/// Handle a key event from Wayland
pub fn handle_key_event(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent) {
pub fn handle_key_event(
&mut self,
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
) -> Option<InputAction> {
// 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)
@@ -185,34 +191,98 @@ impl LockedSurface {
.handle_key_event(keysym, state, modifiers);
match action {
InputAction::SubmitPassword(_password) => {
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();
Some(InputAction::SubmitPassword(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 => {}
InputAction::Cancel => Some(InputAction::Cancel),
InputAction::TempScreenshot => Some(InputAction::TempScreenshot),
InputAction::PasswordChanged => Some(InputAction::PasswordChanged),
InputAction::None => None,
}
}
/// Authenticate a password using PAM
pub fn authenticate_password(&self, password: zeroize::Zeroizing<String>) -> bool {
// Create a simple PAM conversation that provides the password
struct SimpleConversation {
password: Option<zeroize::Zeroizing<String>>,
}
impl pam_client::ConversationHandler for SimpleConversation {
fn init(&mut self, _default_user: Option<impl AsRef<str>>) {}
fn prompt_echo_on(
&mut self,
_msg: &std::ffi::CStr,
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
Err(pam_client::ErrorCode::ABORT)
}
fn prompt_echo_off(
&mut self,
_msg: &std::ffi::CStr,
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
if let Some(pwd) = self.password.take() {
std::ffi::CString::new(pwd.as_str()).map_err(|_| pam_client::ErrorCode::ABORT)
} else {
Err(pam_client::ErrorCode::ABORT)
}
}
fn text_info(&mut self, _msg: &std::ffi::CStr) {}
fn error_msg(&mut self, _msg: &std::ffi::CStr) {}
fn radio_prompt(
&mut self,
_msg: &std::ffi::CStr,
) -> Result<bool, pam_client::ErrorCode> {
Ok(false)
}
}
// Get username
let username = match users::get_current_username() {
Some(name) => name.to_string_lossy().into_owned(),
None => {
log::error!("Failed to get current username");
return false;
}
};
// Create PAM context
let service_name = &self.config.pam_service;
let conversation = SimpleConversation {
password: Some(password),
};
let mut context =
match pam_client::Context::new(service_name, Some(username.as_str()), conversation) {
Ok(ctx) => ctx,
Err(e) => {
log::error!("Failed to initialize PAM context: {:?}", e);
return false;
}
};
// Authenticate
match context.authenticate(pam_client::Flag::NONE) {
Ok(()) => {
log::info!("PAM authentication successful for user {}", username);
true
}
Err(e) => {
log::warn!("PAM authentication failed: {:?}", e);
false
}
}
}
/// Get the input handler for this locked surface
pub fn input_handler(&self) -> &InputHandler {
&self.input_handler
}
/// Get the rendered image surface for this locked surface
pub fn as_image_surface(&self) -> &ImageSurface {
self.renderer.as_image_surface()
@@ -246,7 +316,7 @@ impl LockedSurface {
/// Manager for all locked surfaces (multiple outputs)
pub struct LockManager {
surfaces: Vec<LockedSurface>,
pub surfaces: Vec<LockedSurface>,
config: Config,
locked: bool,
}
@@ -279,13 +349,20 @@ impl LockManager {
}
}
/// 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
/// Handle a key event and return any action that needs processing
/// Returns the first non-None action from any surface
pub fn handle_key_event(
&mut self,
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
) -> Option<InputAction> {
// Distribute key event to all surfaces and collect first action
let mut action = None;
for surface in &mut self.surfaces {
surface.handle_key_event(event.clone());
if let Some(a) = surface.handle_key_event(event.clone()) {
action = Some(a);
}
}
action
}
/// Check if session is locked
+205 -27
View File
@@ -10,9 +10,40 @@ mod util;
use config::Config;
use lock::LockManager;
use std::error::Error;
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::{Arc, Mutex};
use zeroize::Zeroizing;
fn setup_file_logging() {
let log_path =
std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()) + "/.wayrustlock.log";
if let Ok(mut file) = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&log_path)
{
let _ = writeln!(file, "=== wayrustlock log ===");
let _ = writeln!(file, "Started at: {:?}", std::time::SystemTime::now());
}
eprintln!("Logging to: {}", log_path);
}
fn log_to_file(msg: &str) {
let log_path =
std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()) + "/.wayrustlock.log";
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
let _ = writeln!(file, "[{:?}] {}", std::time::SystemTime::now(), msg);
}
}
fn main() -> Result<(), Box<dyn Error>> {
setup_file_logging();
log_to_file("Program starting");
let config = Config::load();
if config.debug {
@@ -21,22 +52,26 @@ fn main() -> Result<(), Box<dyn Error>> {
.init();
} else {
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.filter_level(log::LevelFilter::Debug)
.init();
}
log::info!("Starting wayrustlock v{}", env!("CARGO_PKG_VERSION"));
log_to_file("Starting wayrustlock");
log::info!("Attempting to lock Wayland session...");
log_to_file("Attempting to lock Wayland session");
log::warn!("WARNING: This is a screen locker. To kill it if stuck, use:");
log::warn!(" Method 1: Switch to another TTY (Ctrl+Alt+F2) and kill the process");
log::warn!(" Method 2: Use 'pkill -9 wayrustlock' from another terminal");
log::warn!(" Method 3: Use 'killall wayrustlock'");
log_to_file("Warnings printed");
let lock_manager = Arc::new(Mutex::new(LockManager::new(config.clone())));
let ctrlc_exit = Arc::new(std::sync::atomic::AtomicBool::new(false));
if let Err(e) = lock_wayland_session(config.clone(), lock_manager.clone(), ctrlc_exit) {
log::error!("Failed to lock Wayland session: {}", e);
log_to_file(&format!("Failed to lock Wayland session: {}", e));
log::warn!("Falling back to demonstration mode");
run_demonstration_mode(config, lock_manager)?;
}
@@ -53,7 +88,7 @@ fn lock_wayland_session(
compositor::{CompositorHandler, CompositorState},
output::{OutputHandler, OutputState},
reexports::{
calloop::{EventLoop, LoopHandle},
calloop::{channel, EventLoop, LoopHandle},
calloop_wayland_source::WaylandSource,
},
registry::{ProvidesRegistryState, RegistryState},
@@ -92,16 +127,40 @@ fn lock_wayland_session(
config: Config,
ctrlc_exit: Arc<std::sync::atomic::AtomicBool>,
exit: bool,
auth_tx: Option<channel::Sender<Zeroizing<String>>>,
unlocking: bool,
}
impl SessionLockHandler for WaylandLock {
fn locked(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_session_lock: SessionLock,
qh: &QueueHandle<Self>,
mut session_lock: SessionLock,
) {
log::info!("Session locked successfully!");
log::info!("===========================================");
log::info!("Session LOCKED SUCCESSFULLY!");
log::info!("===========================================");
log_to_file("Session LOCKED - creating surfaces");
eprintln!("SESSION LOCKED - creating lock surfaces");
// Take ownership of session_lock
let lock_ref = &mut session_lock;
// Create lock surfaces for all outputs
for output in self.output_state.outputs() {
log_to_file(&format!("Creating lock surface for output"));
let surface = self.compositor_state.create_surface(qh);
let lock_surface = lock_ref.create_lock_surface(surface, &output, qh);
self.lock_surfaces.push(lock_surface);
}
log_to_file(&format!(
"Created {} lock surfaces",
self.lock_surfaces.len()
));
self.session_lock = Some(session_lock);
if let Ok(mut lock_manager) = self.lock_manager.lock() {
lock_manager.initialize_lock_surfaces();
@@ -114,8 +173,17 @@ fn lock_wayland_session(
_qh: &QueueHandle<Self>,
_session_lock: SessionLock,
) {
log::info!("Session unlocked or lock denied");
if self.unlocking {
log::info!("✅ Session successfully unlocked - compositor confirmed");
log_to_file("✅ Session successfully unlocked - compositor confirmed");
self.unlocking = false;
} else {
log::warn!("Session finished without unlock request - lock denied or cancelled");
log_to_file("Session finished without unlock request");
}
self.session_lock = None;
self.exit = true;
log_to_file("Setting exit=true from finished callback");
}
fn configure(
@@ -127,7 +195,11 @@ fn lock_wayland_session(
_serial: u32,
) {
let (width, height) = configure.new_size;
log::debug!("Configuring lock surface: {}x{}", width, height);
log::info!("===========================================");
log::info!("CONFIGURE callback: {}x{}", width, height);
log::info!("===========================================");
log_to_file(&format!("CONFIGURE: {}x{}", width, height));
eprintln!("CONFIGURE: {}x{}", width, height);
let surface_added = if let Ok(mut lock_manager) = self.lock_manager.lock() {
lock_manager.add_surface(width as i32, height as i32)
@@ -155,6 +227,9 @@ fn lock_wayland_session(
if let Some(locked_surface) = lock_manager.get_surface_mut(surface_count - 1) {
locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone());
// Render the surface first
locked_surface.renderer.render();
match locked_surface.renderer.get_pixel_data() {
Ok(pixel_data) => {
let (renderer_width, renderer_height, stride) =
@@ -283,10 +358,14 @@ fn lock_wayland_session(
fn new_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
qh: &QueueHandle<Self>,
output: wl_output::WlOutput,
) {
log::info!("New output detected");
log::info!("New output detected: creating lock surface");
if let Some(ref session_lock) = self.session_lock {
let surface = self.compositor_state.create_surface(qh);
let _lock_surface = session_lock.create_lock_surface(surface, &output, qh);
}
}
fn update_output(
@@ -325,7 +404,8 @@ fn lock_wayland_session(
_keys: &[u32],
_layout_keysyms: &[smithay_client_toolkit::seat::keyboard::Keysym],
) {
log::debug!("Keyboard entered surface (serial: {})", _serial);
log::info!("Keyboard entered surface (serial: {})", _serial);
log_to_file(&format!("Keyboard entered surface (serial: {})", _serial));
}
fn leave(
@@ -336,7 +416,8 @@ fn lock_wayland_session(
_surface: &wl_surface::WlSurface,
_serial: u32,
) {
log::debug!("Keyboard left surface (serial: {})", _serial);
log::info!("Keyboard left surface (serial: {})", _serial);
log_to_file(&format!("Keyboard left surface (serial: {})", _serial));
}
fn press_key(
@@ -347,6 +428,12 @@ fn lock_wayland_session(
_serial: u32,
event: KeyEvent,
) {
log::info!(
"Key pressed (serial: {}, keysym: {:?})",
_serial,
event.keysym
);
log_to_file(&format!("Key pressed: keysym={:?}", event.keysym));
self.handle_key_event(event);
}
@@ -380,7 +467,8 @@ fn lock_wayland_session(
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
_keymap: smithay_client_toolkit::seat::keyboard::Keymap<'_>,
) {
log::debug!("Keymap updated");
log::info!("Keymap updated");
log_to_file("Keymap updated");
}
}
@@ -398,16 +486,62 @@ fn lock_wayland_session(
impl WaylandLock {
fn handle_key_event(&mut self, event: KeyEvent) {
use crate::input::InputAction;
use smithay_client_toolkit::seat::keyboard::Keysym;
log_to_file(&format!(
"handle_key_event called: keysym={:?}",
event.keysym
));
log::debug!("Key event: {:?}", event.keysym);
if event.keysym == Keysym::Return {
log::info!("Enter pressed - submitting password");
} else if event.keysym == Keysym::BackSpace {
log::info!("Backspace pressed");
} else if let Some(input) = event.utf8 {
log::info!("Key pressed: '{}'", input);
log::info!("Enter pressed - submitting password for authentication");
log_to_file("ENTER pressed - submitting password");
if let Ok(mut lock_manager) = self.lock_manager.lock() {
if let Some(InputAction::SubmitPassword(password)) =
lock_manager.handle_key_event(event.clone())
{
// Send password to auth thread for processing
if let Some(tx) = &self.auth_tx {
let _ = tx.send(password);
}
log::debug!("Password submitted for authentication");
}
}
} else {
log::debug!("Non-character keysym pressed: {:?}", event.keysym);
// Distribute to all surfaces (BackSpace, characters, etc.)
let _ = self
.lock_manager
.lock()
.map(|mut lm| lm.handle_key_event(event));
}
}
/// Handle authentication result from the auth thread
fn handle_auth_result(&mut self, success: bool) {
if success {
log::info!("✅ Authentication successful - unlocking session");
log_to_file("✅ Authentication successful - unlocking session");
// Call unlock on the session_lock, but keep it to receive finished event
if let Some(session_lock) = &self.session_lock {
session_lock.unlock();
self.unlocking = true;
log_to_file("Unlock requested - waiting for finished event");
} else {
log::error!("No session_lock available to unlock!");
}
} else {
log::warn!("❌ Authentication failed - wrong password");
log_to_file("❌ Authentication failed - wrong password");
// Show wrong password feedback on all surfaces
if let Ok(mut lock_manager) = self.lock_manager.lock() {
for surface in &mut lock_manager.surfaces {
surface.show_wrong_password();
}
}
}
}
}
@@ -429,11 +563,37 @@ fn lock_wayland_session(
fn new_capability(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_seat: wayland_client::protocol::wl_seat::WlSeat,
qh: &QueueHandle<Self>,
seat: wayland_client::protocol::wl_seat::WlSeat,
capability: smithay_client_toolkit::seat::Capability,
) {
log::debug!("New capability: {:?}", capability);
log::info!("New capability: {:?}", capability);
log_to_file(&format!("New capability: {:?}", capability));
if capability == smithay_client_toolkit::seat::Capability::Keyboard {
log::info!("Setting up keyboard");
log_to_file("Setting up keyboard - trying to get keyboard");
match self.seat_state.get_keyboard_with_repeat(
qh,
&seat,
None,
self.loop_handle.clone(),
Box::new(|state, _wl_kbd, event| {
log::info!("Keyboard repeat event: {:?}", event);
log_to_file(&format!("Keyboard repeat: {:?}", event));
}),
) {
Ok(_keyboard) => {
log::info!("Keyboard created successfully");
log_to_file("Keyboard created successfully");
}
Err(e) => {
log::error!("Failed to create keyboard: {:?}", e);
log_to_file(&format!("Failed to create keyboard: {:?}", e));
}
}
}
}
fn remove_capability(
@@ -477,6 +637,9 @@ fn lock_wayland_session(
let qh: QueueHandle<WaylandLock> = event_queue.handle();
let mut event_loop: EventLoop<WaylandLock> = EventLoop::try_new()?;
// Create the authentication loop in a separate thread
let (auth_tx, auth_rx) = auth::create_and_run_auth_loop();
let mut state = WaylandLock {
loop_handle: event_loop.handle(),
conn: conn.clone(),
@@ -498,6 +661,8 @@ fn lock_wayland_session(
config,
ctrlc_exit: ctrlc_exit.clone(),
exit: false,
auth_tx: Some(auth_tx),
unlocking: false,
};
state.session_lock = Some(
@@ -508,19 +673,32 @@ fn lock_wayland_session(
);
log::info!("Session lock requested, waiting for compositor...");
log_to_file("Session lock requested");
eprintln!("Session lock requested - running event loop");
WaylandSource::new(conn, event_queue).insert(event_loop.handle())?;
// Insert the authentication channel into the event loop
event_loop.handle().insert_source(
auth_rx,
|event: channel::Event<bool>, _metadata, wayland_lock| {
if let channel::Event::Msg(result) = event {
wayland_lock.handle_auth_result(result);
}
},
)?;
log_to_file("Starting event loop");
eprintln!("Starting event loop - press Enter to unlock");
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_to_file("Event loop exited");
log::info!("Event loop exited, exit={}", state.exit);
log::info!("Exiting wayrustlock");
log_to_file("Function completed");
Ok(())
}
+47 -10
View File
@@ -17,6 +17,7 @@ pub struct Renderer {
wrong_password_start: Option<Instant>,
key_highlight_start: Option<Instant>,
background: Option<ImageSurface>,
password_display: String,
}
impl Renderer {
@@ -48,6 +49,7 @@ impl Renderer {
wrong_password_start: None,
key_highlight_start: None,
background: None,
password_display: String::new(),
}
}
@@ -83,10 +85,15 @@ impl Renderer {
self.key_highlight_start = Some(Instant::now());
}
/// Set the password display string (masked)
pub fn set_password_display(&mut self, password: String) {
self.password_display = password;
}
/// 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);
// Clear the surface - draw a VISIBLE color (dark gray) instead of black
self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0);
self.context.paint().expect("Failed to clear surface");
// Draw background if available
@@ -98,14 +105,8 @@ impl Renderer {
.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,
);
// Draw solid color background (dark gray visible color)
self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0);
self.context
.paint()
.expect("Failed to draw solid background");
@@ -121,6 +122,11 @@ impl Renderer {
self.draw_indicator();
}
// Draw password display (if not empty)
if !self.password_display.is_empty() {
self.draw_password_display();
}
// Draw wrong password feedback if active
if self.wrong_password_shown {
self.draw_wrong_password_feedback();
@@ -249,6 +255,37 @@ impl Renderer {
}
}
/// Draw the password display (masked characters)
fn draw_password_display(&self) {
if self.password_display.is_empty() {
return;
}
// Position: below the indicator ring (or centered if no indicator)
let center_x = self.width as f64 / 2.0;
let center_y = self.height as f64 / 2.0;
let radius = self.config.indicator_radius as f64;
let thickness = self.config.indicator_thickness as f64;
// Place password text below the ring
let text_y = center_y + radius + thickness + 40.0; // 40px below ring
self.context.set_font_size(36.0);
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
// Center the text
let extents = self
.context
.text_extents(&self.password_display)
.expect("Failed to get password text extents");
let text_x = center_x - extents.width() / 2.0;
self.context.move_to(text_x, text_y);
self.context
.show_text(&self.password_display)
.expect("Failed to draw password");
}
/// Draw wrong password feedback (red flash)
fn draw_wrong_password_feedback(&self) {
let center_x = self.width as f64 / 2.0;