feat: Add a lot more features
CI / build-ubuntu (, default) (push) Has been cancelled
CI / build-ubuntu (--features networking, networking) (push) Has been cancelled
CI / build-ubuntu (--no-default-features, no-default-features) (push) Has been cancelled
CI / build-debian (push) Has been cancelled
CI / build-fedora (push) Has been cancelled
CI / build-arch (push) Has been cancelled
CI / build-opensuse (push) Has been cancelled

- Add new system.rs module for system monitoring via D-Bus:
  - Battery status (UPower)
  - Media player controls and metadata (MPRIS)
  - WiFi/Bluetooth status (NetworkManager)
  - Keyboard layout tracking
- Add media key support (XF86 Play/Pause/Next/Prev)
- Add function keys F1-F3 for Suspend/Reboot/PowerOff
- Replace deprecated timer.rs with async system management
- Add GitHub Actions CI/CD workflows (CI + Release)
- Update dependencies and add optional networking feature
This commit is contained in:
2026-03-16 14:12:43 +01:00
parent 1dc4d68cf7
commit f489a6e46e
15 changed files with 3582 additions and 829 deletions
+155 -28
View File
@@ -4,12 +4,14 @@ mod input;
mod lock;
mod render;
mod screenshot;
mod system;
mod timer;
mod util;
use config::Config;
use lock::LockManager;
use screenshot::{CaptureData, Screenshot, ScreenshotManager};
use system::SystemManager;
use std::error::Error;
use std::fs::OpenOptions;
use std::io::Write;
@@ -125,6 +127,9 @@ struct WaylandLock {
unlocking: bool,
screenshot_manager: Option<ScreenshotManager>,
grace_until: Option<Instant>,
system_manager: Arc<SystemManager>,
modifiers: Modifiers,
current_layout: u32,
}
impl WaylandLock {
@@ -163,6 +168,41 @@ impl WaylandLock {
}
fn handle_key_event(&mut self, event: KeyEvent) {
use crate::input::InputAction;
use smithay_client_toolkit::seat::keyboard::Keysym;
// Media and Session keys (Always handle these first and don't trigger grace unlock)
match event.keysym {
Keysym::XF86_AudioPlay | Keysym::XF86_AudioPause => {
self.system_manager.media_play_pause();
return;
}
Keysym::XF86_AudioNext => {
self.system_manager.media_next();
return;
}
Keysym::XF86_AudioPrev => {
self.system_manager.media_prev();
return;
}
Keysym::F1 => {
self.system_manager
.send_command(system::SystemCommand::Suspend);
return;
}
Keysym::F2 => {
self.system_manager
.send_command(system::SystemCommand::Reboot);
return;
}
Keysym::F3 => {
self.system_manager
.send_command(system::SystemCommand::PowerOff);
return;
}
_ => {}
}
// Check if we're in the grace period (any key unlocks without password)
if let Some(grace_until) = self.grace_until {
if Instant::now() < grace_until {
@@ -173,16 +213,14 @@ impl WaylandLock {
}
}
use crate::input::InputAction;
use smithay_client_toolkit::seat::keyboard::Keysym;
if event.keysym == Keysym::Return {
log::info!("Enter pressed - submitting password");
if let Ok(mut lock_manager) = self.lock_manager.lock() {
let mut password = Zeroizing::new(String::new());
let modifiers = self.modifiers;
for surface in &mut lock_manager.surfaces {
if let Some(InputAction::SubmitPassword(p)) =
surface.handle_key_event(event.clone())
surface.handle_key_event(event.clone(), modifiers)
{
password = p;
}
@@ -194,17 +232,12 @@ impl WaylandLock {
}
}
} else {
let action = self
let modifiers = self.modifiers;
let _action = self
.lock_manager
.lock()
.map(|mut lm| lm.handle_key_event(event))
.map(|mut lm| lm.handle_key_event(event, modifiers))
.unwrap_or(None);
if let Some(InputAction::TempScreenshot) = action {
if let Ok(mut lm) = self.lock_manager.lock() {
lm.toggle_peek();
}
}
}
}
}
@@ -372,9 +405,11 @@ impl KeyboardHandler for WaylandLock {
_qh: &QueueHandle<Self>,
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
_serial: u32,
_modifiers: Modifiers,
_layout: u32,
modifiers: Modifiers,
layout: u32,
) {
self.modifiers = modifiers;
self.current_layout = layout;
}
}
@@ -402,7 +437,9 @@ impl SeatHandler for WaylandLock {
&seat,
None,
self.loop_handle.clone(),
Box::new(|_state, _kbd, _event| {}),
Box::new(|state, _kbd, event| {
state.handle_key_event(event);
}),
);
}
}
@@ -453,6 +490,7 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
stride,
} => {
let format = format.into_result().unwrap();
let mut info = data.info.lock().unwrap();
*info = Some(screenshot::BufferInfo {
width,
@@ -460,15 +498,22 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
stride,
format,
});
match state
.pool
.create_buffer(width as i32, height as i32, stride as i32, format)
{
Ok((buffer, _canvas)) => {
frame.copy(buffer.wl_buffer());
*data.buffer.lock().unwrap() = Some(buffer);
// Create a dedicated pool for this screenshot to ensure offset 0
// This matches swaylock-effects and fixes "invalid buffer" on some compositors
let size = (stride as usize) * (height as usize);
match SlotPool::new(size, &state.shm_state) {
Ok(mut pool) => {
match pool.create_buffer(width as i32, height as i32, stride as i32, format) {
Ok((buffer, _canvas)) => {
frame.copy(buffer.wl_buffer());
*data.buffer.lock().unwrap() = Some(buffer);
*data.pool.lock().unwrap() = Some(pool);
}
Err(e) => log::error!("Screencopy: Buffer creation failed: {:?}", e),
}
}
Err(e) => log::error!("Screencopy: Buffer creation failed: {:?}", e),
Err(e) => log::error!("Screencopy: Pool creation failed: {:?}", e),
}
}
Event::Flags { flags } => {
@@ -480,13 +525,15 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
let buffer = data.buffer.lock().unwrap().take();
let info = data.info.lock().unwrap().take();
let flags = data.flags.lock().unwrap().take();
if let (Some(buffer), Some(info), Some(flags)) = (buffer, info, flags) {
let pool = data.pool.lock().unwrap().take();
if let (Some(buffer), Some(info), Some(flags), Some(mut pool)) = (buffer, info, flags, pool) {
let handle = screenshot::ScreencopyBufferHandle {
buffer,
info,
y_invert: flags.contains(Flags::YInvert),
};
if let Ok(surface) = mgr.buffer_to_surface(handle, &mut state.pool) {
if let Ok(surface) = mgr.buffer_to_surface(handle, &mut pool) {
let mut ss = Screenshot::new(surface);
let _ = ss.apply_effects(&state.config);
if data.output_idx < state.captured_backgrounds.len() {
@@ -547,11 +594,25 @@ fn main() -> Result<(), Box<dyn Error>> {
let qh: QueueHandle<WaylandLock> = event_queue.handle();
let shm_state = Shm::bind(&globals, &qh).map_err(|_| "wl_shm not supported")?;
let pool = SlotPool::new(1920 * 1080 * 4, &shm_state)?;
let (auth_tx_actual, auth_feedback_rx_actual) = auth::create_and_run_auth_loop();
let system_manager = Arc::new(SystemManager::new());
let (auth_tx_actual, auth_feedback_rx_actual) = match auth::create_and_run_auth_loop() {
Some(channels) => channels,
None => {
log::error!("Failed to initialize authentication. This usually means PAM is not configured correctly.");
log::error!("Please ensure you have a PAM service file at /etc/pam.d/rustlock");
std::process::exit(1);
}
};
let mut event_loop: EventLoop<WaylandLock> = EventLoop::try_new()?;
// Initialize state without pool first
// We'll create the pool after we know the output dimensions
// Initialize pool with a minimal size, it will be resized once outputs are detected
let pool = SlotPool::new(1, &shm_state)?;
let mut state = WaylandLock {
loop_handle: event_loop.handle(),
lock_manager: lock_manager.clone(),
@@ -563,7 +624,7 @@ fn main() -> Result<(), Box<dyn Error>> {
registry_state: RegistryState::new(&globals),
session_lock_state: SessionLockState::new(&globals, &qh),
seat_state: SeatState::new(&globals, &qh),
shm_state,
shm_state: shm_state,
pool,
session_lock: None,
lock_surfaces: Vec::new(),
@@ -575,10 +636,67 @@ fn main() -> Result<(), Box<dyn Error>> {
unlocking: false,
screenshot_manager: ScreenshotManager::new(&globals, &qh).ok(),
grace_until: None,
system_manager: system_manager.clone(),
modifiers: Modifiers::default(),
current_layout: 0,
};
event_queue.blocking_dispatch(&mut state)?;
// Handle custom background image if provided (prioritize over screenshots)
if let Some(ref image_path) = state.config.image {
log::info!("Loading custom background image from {:?}", image_path);
if let Ok(img) = image::open(image_path) {
let img = img.to_rgba8();
let (w, h) = img.dimensions();
let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w as i32, h as i32).unwrap();
{
let mut surface_data = surface.data().unwrap();
for y in 0..h {
for x in 0..w {
let pixel = img.get_pixel(x, y);
let idx = ((y * w + x) * 4) as usize;
surface_data[idx] = pixel[2]; // B
surface_data[idx + 1] = pixel[1]; // G
surface_data[idx + 2] = pixel[0]; // R
surface_data[idx + 3] = pixel[3]; // A
}
}
}
let mut ss = Screenshot::new(surface);
let _ = ss.apply_effects(&state.config);
let surface = ss.into_inner();
let num_outputs = state.output_state.outputs().count();
state.captured_backgrounds = vec![Some(surface); num_outputs];
// Disable screenshots if image was successfully loaded
state.config.screenshots = false;
} else {
log::error!("Failed to load custom background image from {:?}", image_path);
}
}
// Now that we have output info, we can resize the pool if needed
let mut total_size = 0;
for output in state.output_state.outputs() {
if let Some(info) = state.output_state.info(&output) {
if let Some(mode) = info.modes.first() {
let (w, h) = mode.dimensions;
total_size += (w as usize) * (h as usize) * 4;
}
}
}
if total_size > 0 {
// Resize pool to fit all outputs
// SlotPool doesn't have a direct resize, but we can just create a new one if needed,
// or rely on its internal growing if we use it that way.
// Actually SCTK SlotPool handles resizing when creating buffers if needed,
// but it's better to have a large enough base.
state.pool = SlotPool::new(total_size, &state.shm_state)?;
}
let _wayland_source =
WaylandSource::new(conn.clone(), event_queue).insert(event_loop.handle())?;
@@ -606,7 +724,11 @@ fn main() -> Result<(), Box<dyn Error>> {
return calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(100));
}
let mut status = state.system_manager.get_status();
status.keyboard_layout = Some(state.current_layout);
if let Ok(mut lm) = state.lock_manager.lock() {
lm.set_system_status(status);
lm.update();
for surface in &mut lm.surfaces {
let _ = surface.commit(&mut state.pool);
@@ -619,6 +741,11 @@ fn main() -> Result<(), Box<dyn Error>> {
}
})?;
// Dispatch to ensure outputs are ready before capture
if state.config.screenshots {
event_loop.dispatch(Duration::from_millis(50), &mut state)?;
}
if state.config.screenshots {
state.outputs = state.output_state.outputs().collect();
log::info!(