feat: peek-to-reveal on click/ctrl, pre-commit hooks, devShell, fmt fixes
Nightly Release / nightly-build (push) Has been cancelled
Code Quality / quality-checks (push) Has been cancelled
Security Scan / security-audit (push) Has been cancelled

- Click indicator ring to toggle password peek (persistent click-to-toggle)
- Ctrl-hold transient peek refactored to share peek path
- Cursor always visible (previously hidden at position 0, now shows at top of ring)
- .pre-commit-config.yaml: standard hooks + cargo fmt/clippy
- flake.nix: devShell with cargo, rustc, rustfmt, clippy, cargo-audit, prek
- cargo fmt pass across all source files (trailing whitespace, line wrapping)
- MPRIS error logging (silent failures now logged)
- Dependabot versioning-strategy: "increase" → "auto"
This commit is contained in:
2026-06-21 20:44:59 +02:00
parent c2c093595a
commit 833706f5a5
18 changed files with 297 additions and 129 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ updates:
prefix: "deps" prefix: "deps"
reviewers: reviewers:
- "JorySeverijnse" - "JorySeverijnse"
versioning-strategy: "increase" versioning-strategy: "auto"
groups: groups:
rust-dependencies: rust-dependencies:
patterns: patterns:
-1
View File
@@ -39,4 +39,3 @@ jobs:
- name: Check for outdated dependencies - name: Check for outdated dependencies
run: cargo outdated --exit-code 1 || echo "Some dependencies are outdated" run: cargo outdated --exit-code 1 || echo "Some dependencies are outdated"
+27
View File
@@ -0,0 +1,27 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: \.md$
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-added-large-files
- id: check-merge-conflict
- repo: local
hooks:
- id: fmt
name: cargo fmt
entry: cargo fmt
args: ["--", "--check"]
language: system
types: [rust]
pass_filenames: false
- id: clippy
name: cargo clippy
entry: cargo clippy
args: ["--", "-D", "warnings"]
language: system
pass_filenames: false
+21 -2
View File
@@ -28,8 +28,27 @@
nativeBuildInputs = [ nativeBuildInputs = [
pkgs.pkg-config pkgs.pkg-config
pkgs.rustPlatform.bindgenHook pkgs.rustPlatform.bindgenHook
pkgs.rustfmt ];
pkgs.clippy };
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
cargo
rustc
rustfmt
clippy
cargo-audit
cargo-deny
prek
pkg-config
rustPlatform.bindgenHook
cairo
pam
gdk-pixbuf
librsvg
pango
libxkbcommon
dbus
]; ];
}; };
}; };
+6 -7
View File
@@ -13,7 +13,8 @@ use zeroize::Zeroizing;
type AuthChannels = ( type AuthChannels = (
channel::Sender<(Zeroizing<String>, u64)>, channel::Sender<(Zeroizing<String>, u64)>,
channel::Channel<(bool, u64)>, channel::Channel<(bool, u64)>,
);pub struct LockConversation { );
pub struct LockConversation {
pub password: Option<Zeroizing<String>>, pub password: Option<Zeroizing<String>>,
} }
@@ -39,13 +40,10 @@ impl pam_client::ConversationHandler for LockConversation {
} }
} }
pub fn create_and_run_auth_loop( pub fn create_and_run_auth_loop(service_name: String) -> Option<AuthChannels> {
service_name: String,
) -> Option<AuthChannels> {
let username = username(); let username = username();
let (auth_req_send, auth_req_recv) = let (auth_req_send, auth_req_recv) = channel::channel::<(Zeroizing<String>, u64)>();
channel::channel::<(Zeroizing<String>, u64)>();
let (auth_res_send, auth_res_recv) = channel::channel::<(bool, u64)>(); let (auth_res_send, auth_res_recv) = channel::channel::<(bool, u64)>();
thread::spawn(move || { thread::spawn(move || {
@@ -55,7 +53,8 @@ pub fn create_and_run_auth_loop(
// Creating a new context each time is expensive because it // Creating a new context each time is expensive because it
// re-parses configs and re-loads shared libraries for every attempt. // re-parses configs and re-loads shared libraries for every attempt.
let conversation = LockConversation { password: None }; let conversation = LockConversation { password: None };
let mut context = match Context::new(service_name.as_str(), Some(username.as_str()), conversation) { let mut context =
match Context::new(service_name.as_str(), Some(username.as_str()), conversation) {
Ok(ctx) => { Ok(ctx) => {
debug!("Prepared to authenticate user '{}'", username); debug!("Prepared to authenticate user '{}'", username);
ctx ctx
-2
View File
@@ -292,7 +292,6 @@ pub struct Config {
/// Timeout (seconds) for system commands (poweroff, reboot, suspend) /// Timeout (seconds) for system commands (poweroff, reboot, suspend)
#[arg(long, default_value = "5")] #[arg(long, default_value = "5")]
pub command_timeout: u64, pub command_timeout: u64,
} }
impl Config { impl Config {
@@ -347,7 +346,6 @@ impl Config {
config config
} }
} }
#[cfg(test)] #[cfg(test)]
+2 -1
View File
@@ -165,7 +165,8 @@ impl InputHandler {
/// Check if key highlight should be shown /// Check if key highlight should be shown
pub fn should_show_key_highlight(&self) -> bool { pub fn should_show_key_highlight(&self) -> bool {
if let Some(timer) = self.key_highlight_timer { if let Some(timer) = self.key_highlight_timer {
timer.elapsed() < std::time::Duration::from_millis(self.config.key_highlight_window_duration) timer.elapsed()
< std::time::Duration::from_millis(self.config.key_highlight_window_duration)
} else { } else {
false false
} }
+24 -3
View File
@@ -31,6 +31,8 @@ pub struct LockedSurface {
/// Last clock minute (unix-minute) we rendered, to detect %H:%M rollover. /// Last clock minute (unix-minute) we rendered, to detect %H:%M rollover.
last_minute: i64, last_minute: i64,
ctrl_held: bool, ctrl_held: bool,
/// Set by clicking on the indicator ring. Persists until clicked again.
peek_toggled: bool,
} }
impl LockedSurface { impl LockedSurface {
@@ -64,6 +66,7 @@ impl LockedSurface {
dirty: true, dirty: true,
last_minute: i64::MIN, last_minute: i64::MIN,
ctrl_held: false, ctrl_held: false,
peek_toggled: false,
}) })
} }
@@ -182,11 +185,21 @@ impl LockedSurface {
} }
if !self.config.hide_password { if !self.config.hide_password {
let buf = self.input_handler.password_buffer();
let length = self.input_handler.password_length(); let length = self.input_handler.password_length();
if self.ctrl_held { if self.ctrl_held || self.peek_toggled {
self.renderer log::debug!(
.peek_password(self.input_handler.password_buffer().as_str()); "LockedSurface::update: PEEK mode (ctrl={}, toggled={})",
self.ctrl_held,
self.peek_toggled
);
self.renderer.peek_password(buf.as_str());
} else { } else {
log::debug!(
"LockedSurface::update: DOT mode (ctrl={}, toggled={})",
self.ctrl_held,
self.peek_toggled
);
self.renderer.set_password_display(length); self.renderer.set_password_display(length);
} }
} }
@@ -302,6 +315,14 @@ impl LockedSurface {
self.dirty = true; self.dirty = true;
} }
} }
/// Toggle peek mode on/off. Called when the user clicks on the indicator
/// ring. Persists until toggled again (unlike Ctrl-hold which is transient).
pub fn toggle_peek(&mut self) {
self.peek_toggled = !self.peek_toggled;
log::debug!("toggle_peek: peek_toggled = {}", self.peek_toggled);
self.dirty = true;
}
} }
pub struct LockManager { pub struct LockManager {
+56 -9
View File
@@ -65,9 +65,8 @@ fn setup_file_logging(config: &Config) {
} }
} }
} else if config.log_file { } else if config.log_file {
let default_path = std::path::PathBuf::from( let default_path =
std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()), std::path::PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()))
)
.join(".rustlock.log"); .join(".rustlock.log");
match OpenOptions::new() match OpenOptions::new()
.create(true) .create(true)
@@ -506,8 +505,16 @@ impl KeyboardHandler for WaylandLock {
modifiers: Modifiers, modifiers: Modifiers,
layout: u32, layout: u32,
) { ) {
let ctrl_changed = self.modifiers.ctrl != modifiers.ctrl;
self.modifiers = modifiers; self.modifiers = modifiers;
self.current_layout = layout; self.current_layout = layout;
if ctrl_changed {
log::debug!(
"update_modifiers: ctrl {} -> {}",
!modifiers.ctrl,
modifiers.ctrl
);
}
if let Ok(mut lock_manager) = self.lock_manager.lock() { if let Ok(mut lock_manager) = self.lock_manager.lock() {
lock_manager.set_ctrl_held(modifiers.ctrl); lock_manager.set_ctrl_held(modifiers.ctrl);
} }
@@ -650,7 +657,10 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
if let Ok(surface) = mgr.buffer_to_surface(handle, &mut pool) { if let Ok(surface) = mgr.buffer_to_surface(handle, &mut pool) {
let mut ss = Screenshot::new(surface); let mut ss = Screenshot::new(surface);
if let Err(e) = ss.apply_effects(&state.config) { if let Err(e) = ss.apply_effects(&state.config) {
log::error!("Failed to apply effects to screenshot {}: {e}", data.output_idx); log::error!(
"Failed to apply effects to screenshot {}: {e}",
data.output_idx
);
} }
if data.output_idx < state.captured_backgrounds.len() { if data.output_idx < state.captured_backgrounds.len() {
state.captured_backgrounds[data.output_idx] = Some(ss.into_inner()); state.captured_backgrounds[data.output_idx] = Some(ss.into_inner());
@@ -700,6 +710,8 @@ impl PointerHandler for WaylandLock {
if let Ok(lm) = self.lock_manager.lock() { if let Ok(lm) = self.lock_manager.lock() {
for surface in &lm.surfaces { for surface in &lm.surfaces {
if surface.matches_surface(&event.surface) { if surface.matches_surface(&event.surface) {
// Check media controls first
let mut handled = false;
for (action, rx, ry, rw, rh) in &surface.renderer.media_rects { for (action, rx, ry, rw, rh) in &surface.renderer.media_rects {
if x >= *rx && x <= rx + rw && y >= *ry && y <= ry + rh { if x >= *rx && x <= rx + rw && y >= *ry && y <= ry + rh {
match *action { match *action {
@@ -709,8 +721,34 @@ impl PointerHandler for WaylandLock {
"prev" => self.system_manager.media_prev(), "prev" => self.system_manager.media_prev(),
_ => {} _ => {}
} }
handled = true;
break;
}
}
if handled {
return; return;
} }
// Check indicator ring hit — toggle password peek
let cx = surface.renderer.width as f64 / 2.0;
let cy = surface.renderer.height as f64 / 2.0;
let r = surface.renderer.config.indicator_radius as f64;
let dx = x - cx;
let dy = y - cy;
if dx * dx + dy * dy <= r * r {
// Need to reborrow mutably for toggle
drop(lm);
if let Ok(mut lm) = self.lock_manager.lock() {
if let Some(s) = lm
.surfaces
.iter_mut()
.find(|s| s.matches_surface(&event.surface))
{
s.toggle_peek();
s.update();
let _ = s.commit(&mut self.pool);
}
}
return;
} }
} }
} }
@@ -757,12 +795,16 @@ fn main() -> Result<(), Box<dyn Error>> {
let system_manager = Arc::new(SystemManager::new(&config)); let system_manager = Arc::new(SystemManager::new(&config));
let (auth_tx_actual, auth_feedback_rx_actual) = let (auth_tx_actual, auth_feedback_rx_actual) = match auth::create_and_run_auth_loop(
match auth::create_and_run_auth_loop(config.pam_service.clone()) { config.pam_service.clone(),
) {
Some(channels) => channels, Some(channels) => channels,
None => { None => {
log::error!("Failed to initialize authentication. This usually means PAM is not configured correctly."); 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/{}", config.pam_service); log::error!(
"Please ensure you have a PAM service file at /etc/pam.d/{}",
config.pam_service
);
std::process::exit(1); std::process::exit(1);
} }
}; };
@@ -902,8 +944,13 @@ fn main() -> Result<(), Box<dyn Error>> {
// treat as auth failure so the user gets feedback instead of hanging forever. // treat as auth failure so the user gets feedback instead of hanging forever.
if state.auth_pending_seq.is_some() { if state.auth_pending_seq.is_some() {
if let Some(at) = state.auth_pending_at { if let Some(at) = state.auth_pending_at {
if Instant::now().duration_since(at) >= Duration::from_millis(state.config.auth_timeout) { if Instant::now().duration_since(at)
log::warn!("Authentication timed out after {} ms", state.config.auth_timeout); >= Duration::from_millis(state.config.auth_timeout)
{
log::warn!(
"Authentication timed out after {} ms",
state.config.auth_timeout
);
// Clear pending seq so the eventual PAM result is ignored as stale // Clear pending seq so the eventual PAM result is ignored as stale
state.auth_pending_seq = None; state.auth_pending_seq = None;
state.handle_auth_result(false); state.handle_auth_result(false);
+10 -5
View File
@@ -12,8 +12,7 @@ impl Renderer {
if a > 0.0 { if a > 0.0 {
self.context.new_path(); self.context.new_path();
self.context self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
.set_source_rgba(r, g, b, a * self.fade_alpha);
self.context.set_line_width(thickness + 2.0); self.context.set_line_width(thickness + 2.0);
self.context.set_line_join(cairo::LineJoin::Round); self.context.set_line_join(cairo::LineJoin::Round);
ring_shape::build_ring_path( ring_shape::build_ring_path(
@@ -180,19 +179,25 @@ impl Renderer {
pub(crate) fn update_feedback_timers(&mut self) { pub(crate) fn update_feedback_timers(&mut self) {
self.update_uptime(); self.update_uptime();
if let Some(start) = self.wrong_password_start { if let Some(start) = self.wrong_password_start {
if start.elapsed() > std::time::Duration::from_millis(self.config.wrong_password_duration) { if start.elapsed()
> std::time::Duration::from_millis(self.config.wrong_password_duration)
{
self.wrong_password_shown = false; self.wrong_password_shown = false;
self.wrong_password_start = None; self.wrong_password_start = None;
} }
} }
if let Some(start) = self.key_highlight_start { if let Some(start) = self.key_highlight_start {
if start.elapsed() > std::time::Duration::from_millis(self.config.key_highlight_duration) { if start.elapsed()
> std::time::Duration::from_millis(self.config.key_highlight_duration)
{
self.key_highlight_shown = false; self.key_highlight_shown = false;
self.key_highlight_start = None; self.key_highlight_start = None;
} }
} }
if let Some(start) = self.cleared_feedback_start { if let Some(start) = self.cleared_feedback_start {
if start.elapsed() > std::time::Duration::from_millis(self.config.cleared_feedback_duration) { if start.elapsed()
> std::time::Duration::from_millis(self.config.cleared_feedback_duration)
{
self.cleared_feedback_shown = false; self.cleared_feedback_shown = false;
self.cleared_feedback_start = None; self.cleared_feedback_start = None;
} }
+28 -7
View File
@@ -75,17 +75,37 @@ impl Renderer {
let thickness = self.config.indicator_thickness as f64; let thickness = self.config.indicator_thickness as f64;
let shape = self.config.ring_shape; let shape = self.config.ring_shape;
self.context.new_path(); let max_dots = self.config.max_dots as f64;
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha); let dot_radius = radius - thickness - 10.0;
let t_offset = ring_shape::top_centre_offset(shape);
let count = self.password_display.len(); let count = self.password_display.len();
if count == 0 { if count == 0 {
return; return;
} }
let max_dots = self.config.max_dots as f64; if self.peeking {
let dot_radius = radius - thickness - 10.0; log::debug!("draw_password_display: PEEKING text ({} chars)", count);
let t_offset = ring_shape::top_centre_offset(shape); // Draw each character at the same ring-perimeter positions as dots
self.context.set_font_size(11.0);
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
for (i, ch) in self.password_display.chars().enumerate() {
let t = (i as f64 / max_dots) + t_offset;
let (x, y) = ring_shape::perimeter_point(center_x, center_y, dot_radius, shape, t);
let mut ch_str = String::new();
ch_str.push(ch);
let te = render_try!(self.context.text_extents(&ch_str));
self.context.new_path();
self.context.move_to(
x - te.x_bearing() - te.width() / 2.0,
y - te.y_bearing() - te.height() / 2.0,
);
render_try!(self.context.show_text(&ch_str));
}
} else {
self.context.new_path();
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
for i in 0..count { for i in 0..count {
let t = (i as f64 / max_dots) + t_offset; let t = (i as f64 / max_dots) + t_offset;
@@ -95,9 +115,10 @@ impl Renderer {
self.context.arc(x, y, 4.0, 0.0, 2.0 * std::f64::consts::PI); self.context.arc(x, y, 4.0, 0.0, 2.0 * std::f64::consts::PI);
render_try!(self.context.fill()); render_try!(self.context.fill());
} }
}
// Cursor indicator // Cursor indicator (shared between peek and dot modes)
if self.fade_alpha > 0.0 && self.cursor_position > 0 { if self.fade_alpha > 0.0 {
let cursor_t = ((self.cursor_position as f64 - 0.5) / max_dots) + t_offset; let cursor_t = ((self.cursor_position as f64 - 0.5) / max_dots) + t_offset;
let (cx, cy) = let (cx, cy) =
ring_shape::perimeter_point(center_x, center_y, dot_radius, shape, cursor_t); ring_shape::perimeter_point(center_x, center_y, dot_radius, shape, cursor_t);
+26 -9
View File
@@ -15,7 +15,9 @@ impl Renderer {
if let Ok(img) = image::load_from_memory(data) { if let Ok(img) = image::load_from_memory(data) {
let img = img.to_rgba8(); let img = img.to_rgba8();
let (w, h) = img.dimensions(); let (w, h) = img.dimensions();
if let Ok(mut surface) = ImageSurface::create(Format::ARgb32, w as i32, h as i32) { if let Ok(mut surface) =
ImageSurface::create(Format::ARgb32, w as i32, h as i32)
{
if let Ok(mut surface_data) = surface.data() { if let Ok(mut surface_data) = surface.data() {
for y in 0..h { for y in 0..h {
for x in 0..w { for x in 0..w {
@@ -72,7 +74,8 @@ impl Renderer {
} }
} }
self.context.move_to(text_center_x - te.width() / 2.0, start_y + 20.0); self.context
.move_to(text_center_x - te.width() / 2.0, start_y + 20.0);
render_try!(self.context.show_text(&display_text)); render_try!(self.context.show_text(&display_text));
// All media buttons on one row, evenly spaced. // All media buttons on one row, evenly spaced.
@@ -82,17 +85,18 @@ impl Renderer {
let btn_y = start_y + 50.0; let btn_y = start_y + 50.0;
// Layout: prev | play_pause | next (centered as a group) // Layout: prev | play_pause | next (centered as a group)
let total_buttons: f64 = let total_buttons: f64 = (self.media_prev_icon_surface.is_some() as u32
(self.media_prev_icon_surface.is_some() as u32
+ 1 + 1
+ self.media_next_icon_surface.is_some() as u32) as f64; + self.media_next_icon_surface.is_some() as u32)
as f64;
let group_width = (total_buttons - 1.0) * btn_gap + btn_size; let group_width = (total_buttons - 1.0) * btn_gap + btn_size;
let group_start_x = center_x - group_width / 2.0; let group_start_x = center_x - group_width / 2.0;
let mut btn_x = group_start_x; let mut btn_x = group_start_x;
if let Some(ref icon) = self.media_prev_icon_surface { if let Some(ref icon) = self.media_prev_icon_surface {
self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon); self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon);
self.media_rects.push(("prev", btn_x, btn_y - btn_size / 2.0, btn_size, btn_size)); self.media_rects
.push(("prev", btn_x, btn_y - btn_size / 2.0, btn_size, btn_size));
btn_x += btn_gap; btn_x += btn_gap;
} }
@@ -100,17 +104,30 @@ impl Renderer {
if self.system_status.media_playing { if self.system_status.media_playing {
if let Some(ref icon) = self.media_pause_icon_surface { if let Some(ref icon) = self.media_pause_icon_surface {
self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon); self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon);
self.media_rects.push(("play_pause", btn_x, btn_y - btn_size / 2.0, btn_size, btn_size)); self.media_rects.push((
"play_pause",
btn_x,
btn_y - btn_size / 2.0,
btn_size,
btn_size,
));
} }
} else if let Some(ref icon) = self.media_play_icon_surface { } else if let Some(ref icon) = self.media_play_icon_surface {
self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon); self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon);
self.media_rects.push(("play_pause", btn_x, btn_y - btn_size / 2.0, btn_size, btn_size)); self.media_rects.push((
"play_pause",
btn_x,
btn_y - btn_size / 2.0,
btn_size,
btn_size,
));
} }
btn_x += btn_gap; btn_x += btn_gap;
if let Some(ref icon) = self.media_next_icon_surface { if let Some(ref icon) = self.media_next_icon_surface {
self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon); self.draw_icon_at(btn_x, btn_y - btn_size / 2.0, icon);
self.media_rects.push(("next", btn_x, btn_y - btn_size / 2.0, btn_size, btn_size)); self.media_rects
.push(("next", btn_x, btn_y - btn_size / 2.0, btn_size, btn_size));
} }
} }
} }
+9
View File
@@ -42,6 +42,7 @@ pub struct Renderer {
pub(crate) key_highlight_angle: f64, pub(crate) key_highlight_angle: f64,
pub(crate) background: Option<ImageSurface>, pub(crate) background: Option<ImageSurface>,
pub(crate) password_display: String, pub(crate) password_display: String,
pub(crate) peeking: bool,
pub(crate) cursor_position: usize, pub(crate) cursor_position: usize,
pub(crate) uptime_cache: String, pub(crate) uptime_cache: String,
pub(crate) last_uptime_update: Option<Instant>, pub(crate) last_uptime_update: Option<Instant>,
@@ -85,6 +86,7 @@ impl Renderer {
key_highlight_angle: 0.0, key_highlight_angle: 0.0,
background: None, background: None,
password_display: String::new(), password_display: String::new(),
peeking: false,
cursor_position: 0, cursor_position: 0,
uptime_cache: String::new(), uptime_cache: String::new(),
last_uptime_update: None, last_uptime_update: None,
@@ -126,11 +128,18 @@ impl Renderer {
} }
pub fn set_password_display(&mut self, length: usize) { pub fn set_password_display(&mut self, length: usize) {
log::debug!("Renderer::set_password_display(length={})", length);
self.password_display = ".".repeat(length); self.password_display = ".".repeat(length);
self.peeking = false;
} }
pub fn peek_password(&mut self, password: &str) { pub fn peek_password(&mut self, password: &str) {
log::debug!(
"Renderer::peek_password(len={}, peeking=true)",
password.len()
);
self.password_display = password.to_string(); self.password_display = password.to_string();
self.peeking = true;
} }
pub fn set_cursor_position(&mut self, position: usize) { pub fn set_cursor_position(&mut self, position: usize) {
+3 -10
View File
@@ -9,13 +9,7 @@ const SEGMENTS: usize = 120;
/// Return the (x, y) point on the shape's perimeter at normalized position `t` ∈ [0, 1]. /// Return the (x, y) point on the shape's perimeter at normalized position `t` ∈ [0, 1].
/// `t = 0` is a reference point (rightmost for most shapes); `t` increases clockwise. /// `t = 0` is a reference point (rightmost for most shapes); `t` increases clockwise.
/// `r` is the shape's characteristic radius (distance from center to side/vertex). /// `r` is the shape's characteristic radius (distance from center to side/vertex).
pub(crate) fn perimeter_point( pub(crate) fn perimeter_point(cx: f64, cy: f64, r: f64, shape: RingShape, t: f64) -> (f64, f64) {
cx: f64,
cy: f64,
r: f64,
shape: RingShape,
t: f64,
) -> (f64, f64) {
// Normalize t to [0, 1). Rust's % preserves sign, and the shape-specific // Normalize t to [0, 1). Rust's % preserves sign, and the shape-specific
// functions use floor/truncation that break on negative values. // functions use floor/truncation that break on negative values.
let t = t - t.floor(); let t = t - t.floor();
@@ -95,9 +89,8 @@ fn hexagon_perimeter_point(cx: f64, cy: f64, r: f64, t: f64) -> (f64, f64) {
let local = t * 6.0 - side as f64; let local = t * 6.0 - side as f64;
// Shared helper for edges between two vertices // Shared helper for edges between two vertices
let vert = |angle_rad: f64| -> (f64, f64) { let vert =
(cx + r * angle_rad.cos(), cy + r * angle_rad.sin()) |angle_rad: f64| -> (f64, f64) { (cx + r * angle_rad.cos(), cy + r * angle_rad.sin()) };
};
// Vertices clockwise from right (angle = 0) // Vertices clockwise from right (angle = 0)
let v = [ let v = [
+18 -12
View File
@@ -92,7 +92,10 @@ impl Screenshot {
} }
} }
let mut surface_data = self.surface.data().context("swirl: failed to write surface data")?; let mut surface_data = self
.surface
.data()
.context("swirl: failed to write surface data")?;
surface_data.copy_from_slice(&data); surface_data.copy_from_slice(&data);
Ok(()) Ok(())
} }
@@ -131,7 +134,10 @@ impl Screenshot {
} }
} }
let mut surface_data = self.surface.data().context("melting: failed to write surface data")?; let mut surface_data = self
.surface
.data()
.context("melting: failed to write surface data")?;
surface_data.copy_from_slice(&data); surface_data.copy_from_slice(&data);
Ok(()) Ok(())
} }
@@ -194,7 +200,10 @@ impl Screenshot {
} }
} }
let mut surface_data = self.surface.data().context("pixelate: failed to write surface data")?; let mut surface_data = self
.surface
.data()
.context("pixelate: failed to write surface data")?;
surface_data.copy_from_slice(&data); surface_data.copy_from_slice(&data);
Ok(()) Ok(())
} }
@@ -231,18 +240,12 @@ impl Screenshot {
.context("blur: failed to create image buffer")?; .context("blur: failed to create image buffer")?;
for _ in 0..times { for _ in 0..times {
let mut rgb_data: Vec<[u8; 3]> = let mut rgb_data: Vec<[u8; 3]> = Vec::with_capacity(width * height);
Vec::with_capacity(width * height);
for pixel in img.pixels() { for pixel in img.pixels() {
rgb_data.push([pixel[0], pixel[1], pixel[2]]); rgb_data.push([pixel[0], pixel[1], pixel[2]]);
} }
fastblur::gaussian_blur( fastblur::gaussian_blur(&mut rgb_data, width, height, radius as f32);
&mut rgb_data,
width,
height,
radius as f32,
);
for (i, pixel) in img.pixels_mut().enumerate() { for (i, pixel) in img.pixels_mut().enumerate() {
pixel[0] = rgb_data[i][0]; pixel[0] = rgb_data[i][0];
@@ -292,7 +295,10 @@ impl Screenshot {
} }
} }
let mut surface_data = self.surface.data().context("vignette: failed to write surface data")?; let mut surface_data = self
.surface
.data()
.context("vignette: failed to write surface data")?;
surface_data.copy_from_slice(&data); surface_data.copy_from_slice(&data);
Ok(()) Ok(())
} }
+18 -12
View File
@@ -296,16 +296,25 @@ impl SystemManager {
let action = cmd; let action = cmd;
// Fire-and-forget: don't block the polling loop on MPRIS. // Fire-and-forget: don't block the polling loop on MPRIS.
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
if let Ok(finder) = PlayerFinder::new() { match PlayerFinder::new() {
if let Ok(player) = finder.find_active() { Ok(finder) => {
match action { match finder.find_active() {
BackendCommand::MediaPlayPause => { let _ = player.play_pause(); } Ok(player) => {
BackendCommand::MediaStop => { let _ = player.stop(); } let result = match action {
BackendCommand::MediaNext => { let _ = player.next(); } BackendCommand::MediaPlayPause => player.play_pause(),
BackendCommand::MediaPrev => { let _ = player.previous(); } BackendCommand::MediaStop => player.stop(),
_ => {} BackendCommand::MediaNext => player.next(),
BackendCommand::MediaPrev => player.previous(),
_ => Ok(()),
};
if let Err(e) = result {
error!("MPRIS command {action:?} failed: {e}");
} }
} }
Err(e) => error!("No active MPRIS player found: {e}"),
}
}
Err(e) => error!("Failed to create MPRIS PlayerFinder: {e}"),
} }
}); });
} }
@@ -320,10 +329,7 @@ impl SystemManager {
} }
pub fn get_status(&self) -> SystemStatus { pub fn get_status(&self) -> SystemStatus {
self.status self.status.lock().map(|s| s.clone()).unwrap_or_default()
.lock()
.map(|s| s.clone())
.unwrap_or_default()
} }
pub fn send_command(&self, cmd: BackendCommand) { pub fn send_command(&self, cmd: BackendCommand) {