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"
|
prefix: "deps"
|
||||||
reviewers:
|
reviewers:
|
||||||
- "JorySeverijnse"
|
- "JorySeverijnse"
|
||||||
versioning-strategy: "increase"
|
versioning-strategy: "auto"
|
||||||
groups:
|
groups:
|
||||||
rust-dependencies:
|
rust-dependencies:
|
||||||
patterns:
|
patterns:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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 = [
|
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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user