feat: peek-to-reveal on click/ctrl, pre-commit hooks, devShell, fmt fixes
- 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:
@@ -16,7 +16,7 @@ updates:
|
||||
prefix: "deps"
|
||||
reviewers:
|
||||
- "JorySeverijnse"
|
||||
versioning-strategy: "increase"
|
||||
versioning-strategy: "auto"
|
||||
groups:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
|
||||
@@ -39,4 +39,3 @@ jobs:
|
||||
|
||||
- name: Check for outdated dependencies
|
||||
run: cargo outdated --exit-code 1 || echo "Some dependencies are outdated"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -28,8 +28,27 @@
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
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
@@ -13,7 +13,8 @@ use zeroize::Zeroizing;
|
||||
type AuthChannels = (
|
||||
channel::Sender<(Zeroizing<String>, u64)>,
|
||||
channel::Channel<(bool, u64)>,
|
||||
);pub struct LockConversation {
|
||||
);
|
||||
pub struct LockConversation {
|
||||
pub password: Option<Zeroizing<String>>,
|
||||
}
|
||||
|
||||
@@ -39,13 +40,10 @@ impl pam_client::ConversationHandler for LockConversation {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_and_run_auth_loop(
|
||||
service_name: String,
|
||||
) -> Option<AuthChannels> {
|
||||
pub fn create_and_run_auth_loop(service_name: String) -> Option<AuthChannels> {
|
||||
let username = username();
|
||||
|
||||
let (auth_req_send, auth_req_recv) =
|
||||
channel::channel::<(Zeroizing<String>, u64)>();
|
||||
let (auth_req_send, auth_req_recv) = channel::channel::<(Zeroizing<String>, u64)>();
|
||||
let (auth_res_send, auth_res_recv) = channel::channel::<(bool, u64)>();
|
||||
|
||||
thread::spawn(move || {
|
||||
@@ -55,7 +53,8 @@ pub fn create_and_run_auth_loop(
|
||||
// Creating a new context each time is expensive because it
|
||||
// re-parses configs and re-loads shared libraries for every attempt.
|
||||
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) => {
|
||||
debug!("Prepared to authenticate user '{}'", username);
|
||||
ctx
|
||||
|
||||
@@ -292,7 +292,6 @@ pub struct Config {
|
||||
/// Timeout (seconds) for system commands (poweroff, reboot, suspend)
|
||||
#[arg(long, default_value = "5")]
|
||||
pub command_timeout: u64,
|
||||
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -347,7 +346,6 @@ impl Config {
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+2
-1
@@ -165,7 +165,8 @@ impl InputHandler {
|
||||
/// 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(self.config.key_highlight_window_duration)
|
||||
timer.elapsed()
|
||||
< std::time::Duration::from_millis(self.config.key_highlight_window_duration)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
+24
-3
@@ -31,6 +31,8 @@ pub struct LockedSurface {
|
||||
/// Last clock minute (unix-minute) we rendered, to detect %H:%M rollover.
|
||||
last_minute: i64,
|
||||
ctrl_held: bool,
|
||||
/// Set by clicking on the indicator ring. Persists until clicked again.
|
||||
peek_toggled: bool,
|
||||
}
|
||||
|
||||
impl LockedSurface {
|
||||
@@ -64,6 +66,7 @@ impl LockedSurface {
|
||||
dirty: true,
|
||||
last_minute: i64::MIN,
|
||||
ctrl_held: false,
|
||||
peek_toggled: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,11 +185,21 @@ impl LockedSurface {
|
||||
}
|
||||
|
||||
if !self.config.hide_password {
|
||||
let buf = self.input_handler.password_buffer();
|
||||
let length = self.input_handler.password_length();
|
||||
if self.ctrl_held {
|
||||
self.renderer
|
||||
.peek_password(self.input_handler.password_buffer().as_str());
|
||||
if self.ctrl_held || self.peek_toggled {
|
||||
log::debug!(
|
||||
"LockedSurface::update: PEEK mode (ctrl={}, toggled={})",
|
||||
self.ctrl_held,
|
||||
self.peek_toggled
|
||||
);
|
||||
self.renderer.peek_password(buf.as_str());
|
||||
} else {
|
||||
log::debug!(
|
||||
"LockedSurface::update: DOT mode (ctrl={}, toggled={})",
|
||||
self.ctrl_held,
|
||||
self.peek_toggled
|
||||
);
|
||||
self.renderer.set_password_display(length);
|
||||
}
|
||||
}
|
||||
@@ -302,6 +315,14 @@ impl LockedSurface {
|
||||
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 {
|
||||
|
||||
+56
-9
@@ -65,9 +65,8 @@ fn setup_file_logging(config: &Config) {
|
||||
}
|
||||
}
|
||||
} else if config.log_file {
|
||||
let default_path = std::path::PathBuf::from(
|
||||
std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()),
|
||||
)
|
||||
let default_path =
|
||||
std::path::PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()))
|
||||
.join(".rustlock.log");
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
@@ -506,8 +505,16 @@ impl KeyboardHandler for WaylandLock {
|
||||
modifiers: Modifiers,
|
||||
layout: u32,
|
||||
) {
|
||||
let ctrl_changed = self.modifiers.ctrl != modifiers.ctrl;
|
||||
self.modifiers = modifiers;
|
||||
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() {
|
||||
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) {
|
||||
let mut ss = Screenshot::new(surface);
|
||||
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() {
|
||||
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() {
|
||||
for surface in &lm.surfaces {
|
||||
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 {
|
||||
if x >= *rx && x <= rx + rw && y >= *ry && y <= ry + rh {
|
||||
match *action {
|
||||
@@ -709,8 +721,34 @@ impl PointerHandler for WaylandLock {
|
||||
"prev" => self.system_manager.media_prev(),
|
||||
_ => {}
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if handled {
|
||||
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 (auth_tx_actual, auth_feedback_rx_actual) =
|
||||
match auth::create_and_run_auth_loop(config.pam_service.clone()) {
|
||||
let (auth_tx_actual, auth_feedback_rx_actual) = match auth::create_and_run_auth_loop(
|
||||
config.pam_service.clone(),
|
||||
) {
|
||||
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/{}", config.pam_service);
|
||||
log::error!(
|
||||
"Please ensure you have a PAM service file at /etc/pam.d/{}",
|
||||
config.pam_service
|
||||
);
|
||||
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.
|
||||
if state.auth_pending_seq.is_some() {
|
||||
if let Some(at) = state.auth_pending_at {
|
||||
if Instant::now().duration_since(at) >= Duration::from_millis(state.config.auth_timeout) {
|
||||
log::warn!("Authentication timed out after {} ms", state.config.auth_timeout);
|
||||
if Instant::now().duration_since(at)
|
||||
>= 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
|
||||
state.auth_pending_seq = None;
|
||||
state.handle_auth_result(false);
|
||||
|
||||
+10
-5
@@ -12,8 +12,7 @@ impl Renderer {
|
||||
|
||||
if a > 0.0 {
|
||||
self.context.new_path();
|
||||
self.context
|
||||
.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||
self.context.set_line_width(thickness + 2.0);
|
||||
self.context.set_line_join(cairo::LineJoin::Round);
|
||||
ring_shape::build_ring_path(
|
||||
@@ -180,19 +179,25 @@ impl Renderer {
|
||||
pub(crate) fn update_feedback_timers(&mut self) {
|
||||
self.update_uptime();
|
||||
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_start = None;
|
||||
}
|
||||
}
|
||||
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_start = None;
|
||||
}
|
||||
}
|
||||
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_start = None;
|
||||
}
|
||||
|
||||
+28
-7
@@ -75,17 +75,37 @@ impl Renderer {
|
||||
let thickness = self.config.indicator_thickness as f64;
|
||||
let shape = self.config.ring_shape;
|
||||
|
||||
self.context.new_path();
|
||||
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||
let max_dots = self.config.max_dots as f64;
|
||||
let dot_radius = radius - thickness - 10.0;
|
||||
let t_offset = ring_shape::top_centre_offset(shape);
|
||||
|
||||
let count = self.password_display.len();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let max_dots = self.config.max_dots as f64;
|
||||
let dot_radius = radius - thickness - 10.0;
|
||||
let t_offset = ring_shape::top_centre_offset(shape);
|
||||
if self.peeking {
|
||||
log::debug!("draw_password_display: PEEKING text ({} chars)", count);
|
||||
// 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 {
|
||||
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);
|
||||
render_try!(self.context.fill());
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor indicator
|
||||
if self.fade_alpha > 0.0 && self.cursor_position > 0 {
|
||||
// Cursor indicator (shared between peek and dot modes)
|
||||
if self.fade_alpha > 0.0 {
|
||||
let cursor_t = ((self.cursor_position as f64 - 0.5) / max_dots) + t_offset;
|
||||
let (cx, cy) =
|
||||
ring_shape::perimeter_point(center_x, center_y, dot_radius, shape, cursor_t);
|
||||
|
||||
+26
-9
@@ -15,7 +15,9 @@ impl Renderer {
|
||||
if let Ok(img) = image::load_from_memory(data) {
|
||||
let img = img.to_rgba8();
|
||||
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() {
|
||||
for y in 0..h {
|
||||
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));
|
||||
|
||||
// All media buttons on one row, evenly spaced.
|
||||
@@ -82,17 +85,18 @@ impl Renderer {
|
||||
let btn_y = start_y + 50.0;
|
||||
|
||||
// Layout: prev | play_pause | next (centered as a group)
|
||||
let total_buttons: f64 =
|
||||
(self.media_prev_icon_surface.is_some() as u32
|
||||
let total_buttons: f64 = (self.media_prev_icon_surface.is_some() as u32
|
||||
+ 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_start_x = center_x - group_width / 2.0;
|
||||
let mut btn_x = group_start_x;
|
||||
|
||||
if let Some(ref icon) = self.media_prev_icon_surface {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -100,17 +104,30 @@ impl Renderer {
|
||||
if self.system_status.media_playing {
|
||||
if let Some(ref icon) = self.media_pause_icon_surface {
|
||||
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 {
|
||||
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;
|
||||
|
||||
if let Some(ref icon) = self.media_next_icon_surface {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ pub struct Renderer {
|
||||
pub(crate) key_highlight_angle: f64,
|
||||
pub(crate) background: Option<ImageSurface>,
|
||||
pub(crate) password_display: String,
|
||||
pub(crate) peeking: bool,
|
||||
pub(crate) cursor_position: usize,
|
||||
pub(crate) uptime_cache: String,
|
||||
pub(crate) last_uptime_update: Option<Instant>,
|
||||
@@ -85,6 +86,7 @@ impl Renderer {
|
||||
key_highlight_angle: 0.0,
|
||||
background: None,
|
||||
password_display: String::new(),
|
||||
peeking: false,
|
||||
cursor_position: 0,
|
||||
uptime_cache: String::new(),
|
||||
last_uptime_update: None,
|
||||
@@ -126,11 +128,18 @@ impl Renderer {
|
||||
}
|
||||
|
||||
pub fn set_password_display(&mut self, length: usize) {
|
||||
log::debug!("Renderer::set_password_display(length={})", length);
|
||||
self.password_display = ".".repeat(length);
|
||||
self.peeking = false;
|
||||
}
|
||||
|
||||
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.peeking = true;
|
||||
}
|
||||
|
||||
pub fn set_cursor_position(&mut self, position: usize) {
|
||||
|
||||
@@ -9,13 +9,7 @@ const SEGMENTS: usize = 120;
|
||||
/// 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.
|
||||
/// `r` is the shape's characteristic radius (distance from center to side/vertex).
|
||||
pub(crate) fn perimeter_point(
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
r: f64,
|
||||
shape: RingShape,
|
||||
t: f64,
|
||||
) -> (f64, f64) {
|
||||
pub(crate) fn perimeter_point(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
|
||||
// functions use floor/truncation that break on negative values.
|
||||
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;
|
||||
|
||||
// Shared helper for edges between two vertices
|
||||
let vert = |angle_rad: f64| -> (f64, f64) {
|
||||
(cx + r * angle_rad.cos(), cy + r * angle_rad.sin())
|
||||
};
|
||||
let vert =
|
||||
|angle_rad: f64| -> (f64, f64) { (cx + r * angle_rad.cos(), cy + r * angle_rad.sin()) };
|
||||
|
||||
// Vertices clockwise from right (angle = 0)
|
||||
let v = [
|
||||
|
||||
+18
-12
@@ -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);
|
||||
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);
|
||||
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);
|
||||
Ok(())
|
||||
}
|
||||
@@ -231,18 +240,12 @@ impl Screenshot {
|
||||
.context("blur: failed to create image buffer")?;
|
||||
|
||||
for _ in 0..times {
|
||||
let mut rgb_data: Vec<[u8; 3]> =
|
||||
Vec::with_capacity(width * height);
|
||||
let mut rgb_data: Vec<[u8; 3]> = Vec::with_capacity(width * height);
|
||||
for pixel in img.pixels() {
|
||||
rgb_data.push([pixel[0], pixel[1], pixel[2]]);
|
||||
}
|
||||
|
||||
fastblur::gaussian_blur(
|
||||
&mut rgb_data,
|
||||
width,
|
||||
height,
|
||||
radius as f32,
|
||||
);
|
||||
fastblur::gaussian_blur(&mut rgb_data, width, height, radius as f32);
|
||||
|
||||
for (i, pixel) in img.pixels_mut().enumerate() {
|
||||
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);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+18
-12
@@ -296,16 +296,25 @@ impl SystemManager {
|
||||
let action = cmd;
|
||||
// Fire-and-forget: don't block the polling loop on MPRIS.
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Ok(finder) = PlayerFinder::new() {
|
||||
if let Ok(player) = finder.find_active() {
|
||||
match action {
|
||||
BackendCommand::MediaPlayPause => { let _ = player.play_pause(); }
|
||||
BackendCommand::MediaStop => { let _ = player.stop(); }
|
||||
BackendCommand::MediaNext => { let _ = player.next(); }
|
||||
BackendCommand::MediaPrev => { let _ = player.previous(); }
|
||||
_ => {}
|
||||
match PlayerFinder::new() {
|
||||
Ok(finder) => {
|
||||
match finder.find_active() {
|
||||
Ok(player) => {
|
||||
let result = match action {
|
||||
BackendCommand::MediaPlayPause => player.play_pause(),
|
||||
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 {
|
||||
self.status
|
||||
.lock()
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or_default()
|
||||
self.status.lock().map(|s| s.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn send_command(&self, cmd: BackendCommand) {
|
||||
|
||||
Reference in New Issue
Block a user