From d22b10f8a40985d83419fe0cc2313f23acb803a7 Mon Sep 17 00:00:00 2001 From: Vitalii Lukyanov Date: Fri, 7 Nov 2025 22:20:54 +0100 Subject: [PATCH 1/2] simple locker, nothing fancy, bit weird when it comes to default pam+fprint behavior on nixos, but i can try to figure it out later --- Cargo.lock | 117 +++++++++++++++++++ Cargo.toml | 6 + flake.nix | 7 +- leaper-launcher/src/lib.rs | 8 +- leaper-lock/Cargo.toml | 32 +++++ leaper-lock/src/lib.rs | 231 +++++++++++++++++++++++++++++++++++++ leaper-mode/src/lib.rs | 46 +++++++- leaper-power/src/lib.rs | 8 +- leaper-runner/src/lib.rs | 8 +- leaper/Cargo.toml | 1 + leaper/src/app.rs | 1 - leaper/src/cli.rs | 1 + leaper/src/main.rs | 4 +- 13 files changed, 459 insertions(+), 11 deletions(-) create mode 100644 leaper-lock/Cargo.toml create mode 100644 leaper-lock/src/lib.rs delete mode 100644 leaper/src/app.rs diff --git a/Cargo.lock b/Cargo.lock index 89614b5..dd06272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3115,6 +3115,40 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "iced_sessionlock" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76924846b3a15533d799b2ea0876ebabd63ecd1580d6a6808065e980c142c7d" +dependencies = [ + "futures", + "iced", + "iced_core", + "iced_futures", + "iced_graphics", + "iced_renderer", + "iced_runtime", + "iced_sessionlock_macros", + "log", + "sessionlockev", + "thiserror 1.0.69", + "tracing", + "window_clipboard", +] + +[[package]] +name = "iced_sessionlock_macros" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6573cd0b318ef1ba1fe9e624a51c8a5d8d4647370baac3e07e6d335b9574889b" +dependencies = [ + "darling 0.20.11", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "iced_tiny_skia" version = "0.13.0" @@ -3660,6 +3694,7 @@ dependencies = [ "clap", "color-eyre", "leaper-launcher", + "leaper-lock", "leaper-mode", "leaper-power", "leaper-runner", @@ -3741,6 +3776,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "leaper-lock" +version = "0.1.0" +dependencies = [ + "chrono", + "directories", + "iced", + "iced_sessionlock", + "leaper-macros", + "leaper-mode", + "leaper-style", + "nix 0.30.1", + "nonstick", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "leaper-macros" version = "0.1.0" @@ -3874,6 +3927,35 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libpam-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114545207fdd55a59a967d9d974960007fce62f6f075f2414bea2589091253f6" +dependencies = [ + "libc", + "libpam-sys-helpers", + "libpam-sys-impls", +] + +[[package]] +name = "libpam-sys-helpers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8790428ad4abc1112412a63c780337b5cd050fc90d497bdea96e8e2e029323a9" +dependencies = [ + "libpam-sys-impls", +] + +[[package]] +name = "libpam-sys-impls" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3007a9936a126c31a182569b6e9a6203f0ac79313a42c136a2421685fd81cb" +dependencies = [ + "libc", +] + [[package]] name = "libredox" version = "0.1.10" @@ -4377,6 +4459,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "nonstick" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b9c180cfd5955c50b64bc2022b418e1392b798cfd3173605fb70ab22e9e4ea" +dependencies = [ + "bitflags 2.10.0", + "libc", + "libpam-sys", + "libpam-sys-helpers", + "libpam-sys-impls", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -6259,6 +6354,28 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "sessionlockev" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac4497b1f067db77f046a1a4b3fe29a7c2cdfbbf604122b00685183e963f2c1" +dependencies = [ + "bitflags 2.10.0", + "calloop 0.14.3", + "calloop-wayland-source 0.4.1", + "log", + "raw-window-handle", + "tempfile", + "thiserror 1.0.69", + "waycrate_xkbkeycode", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-misc", + "wayland-protocols-wlr", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index 45ae6cc..41ee815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "leaper-db", "leaper-executor", "leaper-launcher", + "leaper-lock", "leaper-macros", "leaper-mode", "leaper-power", @@ -20,9 +21,13 @@ name = "leaper" [workspace.dependencies] iced = "0.13" iced_layershell = "0.13.7" +iced_sessionlock = "0.13.7" iced_aw = { version = "0.12.2", default-features = false } iced_fonts = { version = "0.2.1", features = ["nerd"] } +nonstick = "0.1.1" +nix = "0.30.1" + image = "0.25.8" ez-pixmap = "0.2.2" @@ -53,6 +58,7 @@ shlex = "1.3.0" serde = "1.0" toml = "0.9.8" heck = "0.5.0" +chrono = "0.4.42" smart-default = "0.7" derive_more = "2.0.1" diff --git a/flake.nix b/flake.nix index b70beca..986a996 100644 --- a/flake.nix +++ b/flake.nix @@ -67,6 +67,7 @@ (craneLib.fileset.commonCargoSources ./leaper-launcher) (craneLib.fileset.commonCargoSources ./leaper-power) (craneLib.fileset.commonCargoSources ./leaper-runner) + (craneLib.fileset.commonCargoSources ./leaper-lock) (craneLib.fileset.commonCargoSources ./leaper-executor) (craneLib.fileset.commonCargoSources ./leaper-style) (craneLib.fileset.commonCargoSources ./leaper-tracing) @@ -103,7 +104,10 @@ src = fileSet; buildInputs = - libs; + libs + ++ (with pkgs; [ + linux-pam + ]); passthru.runtimeLibsPath = libsPath; postFixup = '' @@ -293,6 +297,7 @@ }; }; }; + security.pam.services.leaper-lock = {}; }; }; }; diff --git a/leaper-launcher/src/lib.rs b/leaper-launcher/src/lib.rs index 0eac528..727010e 100644 --- a/leaper-launcher/src/lib.rs +++ b/leaper-launcher/src/lib.rs @@ -115,12 +115,16 @@ impl LeaperMode for LeaperLauncher { .font(iced_fonts::REQUIRED_FONT_BYTES) .font(iced_fonts::NERD_FONT_BYTES) .executor::() - .run_with(move || Self::init(project_dirs, config))?; + .run_with(move || Self::init(project_dirs, config, ()))?; Ok(()) } - fn init(_project_dirs: ProjectDirs, config: LeaperModeConfig) -> (Self, Self::Task) + fn init( + _project_dirs: ProjectDirs, + config: LeaperModeConfig, + _args: Self::InitArgs, + ) -> (Self, Self::Task) where Self: Sized, { diff --git a/leaper-lock/Cargo.toml b/leaper-lock/Cargo.toml new file mode 100644 index 0000000..e47d805 --- /dev/null +++ b/leaper-lock/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "leaper-lock" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[package.metadata.cargo-machete] +ignored = ["thiserror"] + +[dependencies] +macros = { path = "../leaper-macros", package = "leaper-macros" } +mode = { path = "../leaper-mode", package = "leaper-mode" } +style = { path = "../leaper-style", package = "leaper-style" } + +iced.workspace = true +iced_sessionlock.workspace = true + +tokio.workspace = true + +nonstick.workspace = true +nix = { workspace = true, features = ["user"] } + +directories.workspace = true + +chrono.workspace = true + +tracing.workspace = true + +thiserror.workspace = true diff --git a/leaper-lock/src/lib.rs b/leaper-lock/src/lib.rs new file mode 100644 index 0000000..8547a9a --- /dev/null +++ b/leaper-lock/src/lib.rs @@ -0,0 +1,231 @@ +use std::{sync::Arc, time::Duration}; + +use directories::ProjectDirs; +use iced::{ + Length, + alignment::Horizontal, + keyboard, + widget::{center, column, container, text, text_input}, +}; +use iced_sessionlock::to_session_message; + +use macros::lerror; +use mode::{ + LeaperModeMultiWindow, + config::{LeaperAppModeConfigError, LeaperModeConfig}, +}; +use nonstick::{AuthnFlags, ConversationAdapter, Transaction}; + +pub struct LeaperLock { + config: LeaperModeConfig, + + user_name: String, + password: String, +} + +impl LeaperModeMultiWindow for LeaperLock { + type RunError = LeaperLockError; + type InitArgs = String; + type Msg = LeaperLockMsg; + + fn run() -> Result<(), Self::RunError> { + let project_dirs = + ProjectDirs::from("com", "tukanoid", "leaper").ok_or(Self::RunError::NoProjectDirs)?; + let config = LeaperModeConfig::open(&project_dirs)?; + + let uid = nix::unistd::Uid::current(); + let user = nix::unistd::User::from_uid(uid)?.ok_or(LeaperLockError::NoUserFound)?; + + iced_sessionlock::build_pattern::application(Self::update, Self::view) + .subscription(Self::subscription) + .theme(Self::theme) + .run_with(|| Self::init(project_dirs, config, user.name))?; + + Ok(()) + } + + fn init( + _project_dirs: ProjectDirs, + config: LeaperModeConfig, + user_name: Self::InitArgs, + ) -> (Self, Self::Task) + where + Self: Sized, + { + let lock = Self { + config, + + user_name, + password: String::new(), + }; + let task = Self::Task::none(); + + (lock, task) + } + + fn view(&self, _id: iced::window::Id) -> Self::Element<'_> { + let date_time = chrono::Local::now(); + let time_str = date_time.format("%H:%M:%S").to_string(); + let date_str = date_time.format("%A - %d/%b/%Y").to_string(); + + center( + column![ + center( + column![text(time_str).size(60), text(date_str).size(40)] + .align_x(Horizontal::Center) + .spacing(10) + ) + .padding(15) + .width(Length::Shrink) + .height(Length::Shrink) + .style(|theme| { + let mut style = container::bordered_box(theme); + style.background = None; + style.border = style.border.rounded(10.0).width(2); + + style + }), + text_input("Enter you password...", &self.password) + .width(600.0) + .size(20) + .padding(10.0) + .on_input(LeaperLockMsg::EnterPassword) + .on_submit(LeaperLockMsg::ConfirmPassword) + .secure(true) + .style(style::text_input), + ] + .align_x(Horizontal::Center) + .spacing(50), + ) + .into() + } + + fn update(&mut self, msg: Self::Msg) -> Self::Task { + match msg { + LeaperLockMsg::SecondTick => {} + + LeaperLockMsg::EnterPassword(new_pass) => self.password = new_pass, + LeaperLockMsg::ConfirmPassword => { + if let Err(err) = (|| { + let mut auth = nonstick::TransactionBuilder::new_with_service("leaper-lock") + .username(&self.user_name) + .build( + LeaperAuthAdapter { + user_name: self.user_name.clone(), + password: self.password.clone(), + } + .into_conversation(), + )?; + + auth.authenticate(AuthnFlags::empty())?; + auth.account_management(AuthnFlags::empty())?; + + LeaperLockResult::Ok(()) + })() { + tracing::error!("{err}"); + } + + return Self::Task::done(LeaperLockMsg::UnLock); + } + + LeaperLockMsg::IcedEvent(ev) => match ev { + iced::Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(keyboard::key::Named::Enter), + .. + }) => return Self::Task::done(Self::Msg::ConfirmPassword), + _ => {} + }, + + LeaperLockMsg::UnLock => return Self::Task::done(msg), + } + + Self::Task::none() + } + + fn subscription(&self) -> Self::Subscription { + Self::Subscription::batch([ + iced::event::listen().map(LeaperLockMsg::IcedEvent), + Self::Subscription::run_with_id( + "second-timer", + iced::stream::channel(1, move |mut sender| async move { + loop { + tokio::time::sleep(Duration::from_millis(100)).await; + + if let Err(err) = sender.start_send(LeaperLockMsg::SecondTick) { + tracing::error!( + "Failed to send SecondTick message to main thread: {err}" + ); + } + } + }), + ), + ]) + } + + fn title(&self) -> String { + "Leaper Lock".into() + } + + fn theme(&self) -> mode::LeaperModeTheme { + self.config.theme.clone() + } +} + +pub struct LeaperAuthAdapter { + user_name: String, + password: String, +} + +impl nonstick::ConversationAdapter for LeaperAuthAdapter { + fn prompt( + &self, + _request: impl AsRef, + ) -> nonstick::Result { + Ok((&self.user_name).into()) + } + + fn masked_prompt( + &self, + _request: impl AsRef, + ) -> nonstick::Result { + Ok((&self.password).into()) + } + + fn error_msg(&self, message: impl AsRef) { + tracing::error!("[leaper-lock-auth] {}", message.as_ref().to_string_lossy()) + } + + fn info_msg(&self, message: impl AsRef) { + tracing::info!("[leaper-lock-auth] {}", message.as_ref().to_string_lossy()) + } +} + +#[to_session_message] +#[derive(Debug, Clone)] +pub enum LeaperLockMsg { + SecondTick, + + EnterPassword(String), + ConfirmPassword, + + IcedEvent(iced::Event), +} + +#[lerror] +#[lerr(prefix = "[leaper-lock]", result_name = LeaperLockResult)] +pub enum LeaperLockError { + #[lerr(str = "[iced_sessionlock] {0}")] + SessionLock(#[lerr(from, wrap = Arc)] iced_sessionlock::Error), + #[lerr(str = "[nonstick] {0}")] + Nonstick(#[lerr(from)] nonstick::ErrorCode), + #[lerr(str = "[nix] {0}")] + Nix(#[lerr(from)] nix::Error), + + #[lerr(str = "{0}")] + Config(#[lerr(from)] LeaperAppModeConfigError), + + #[lerr(str = "No ProjectDirs!")] + NoProjectDirs, + #[lerr(str = "No User found!")] + NoUserFound, +} diff --git a/leaper-mode/src/lib.rs b/leaper-mode/src/lib.rs index cf52969..d8f72ee 100644 --- a/leaper-mode/src/lib.rs +++ b/leaper-mode/src/lib.rs @@ -20,15 +20,59 @@ pub trait LeaperMode { where Self: 'a; + type InitArgs = (); type Msg: std::fmt::Debug + Clone; fn run() -> Result<(), Self::RunError>; - fn init(project_dirs: ProjectDirs, config: LeaperModeConfig) -> (Self, Self::Task) + fn init( + project_dirs: ProjectDirs, + config: LeaperModeConfig, + _args: Self::InitArgs, + ) -> (Self, Self::Task) where Self: Sized; fn view(&self) -> Self::Element<'_>; + + fn update(&mut self, msg: Self::Msg) -> Self::Task; + fn subscription(&self) -> Self::Subscription; + + fn title(&self) -> String; + fn theme(&self) -> LeaperModeTheme; + + fn project_dirs() -> ProjectDirs { + ProjectDirs::from("com", "tukanoid", "leaper").unwrap() + } +} + +pub trait LeaperModeMultiWindow { + type RunError; + + type Task = iced::Task; + type Subscription = iced::Subscription; + + type Renderer = iced::Renderer; + type Element<'a> + = iced::Element<'a, Self::Msg, LeaperModeTheme, Self::Renderer> + where + Self: 'a; + + type InitArgs = (); + type Msg: std::fmt::Debug + Clone; + + fn run() -> Result<(), Self::RunError>; + + fn init( + project_dirs: ProjectDirs, + config: LeaperModeConfig, + _args: Self::InitArgs, + ) -> (Self, Self::Task) + where + Self: Sized; + + fn view(&self, id: iced::window::Id) -> Self::Element<'_>; + fn update(&mut self, msg: Self::Msg) -> Self::Task; fn subscription(&self) -> Self::Subscription; diff --git a/leaper-power/src/lib.rs b/leaper-power/src/lib.rs index df1558b..5197395 100644 --- a/leaper-power/src/lib.rs +++ b/leaper-power/src/lib.rs @@ -102,12 +102,16 @@ impl LeaperMode for LeaperPower { .subscription(Self::subscription) .font(iced_fonts::REQUIRED_FONT_BYTES) .font(iced_fonts::NERD_FONT_BYTES) - .run_with(move || Self::init(project_dirs, config))?; + .run_with(move || Self::init(project_dirs, config, ()))?; Ok(()) } - fn init(_project_dirs: ProjectDirs, config: LeaperModeConfig) -> (Self, Self::Task) + fn init( + _project_dirs: ProjectDirs, + config: LeaperModeConfig, + _args: Self::InitArgs, + ) -> (Self, Self::Task) where Self: Sized, { diff --git a/leaper-runner/src/lib.rs b/leaper-runner/src/lib.rs index c724c05..1a14511 100644 --- a/leaper-runner/src/lib.rs +++ b/leaper-runner/src/lib.rs @@ -67,12 +67,16 @@ impl LeaperMode for LeaperRunner { .settings(settings) .theme(Self::theme) .subscription(Self::subscription) - .run_with(move || Self::init(project_dirs, config))?; + .run_with(move || Self::init(project_dirs, config, ()))?; Ok(()) } - fn init(_project_dirs: ProjectDirs, config: LeaperModeConfig) -> (Self, Self::Task) + fn init( + _project_dirs: ProjectDirs, + config: LeaperModeConfig, + _args: Self::InitArgs, + ) -> (Self, Self::Task) where Self: Sized, { diff --git a/leaper/Cargo.toml b/leaper/Cargo.toml index f69a88f..af629d3 100644 --- a/leaper/Cargo.toml +++ b/leaper/Cargo.toml @@ -17,6 +17,7 @@ mode = { path = "../leaper-mode", package = "leaper-mode" } launcher = { path = "../leaper-launcher", package = "leaper-launcher" } power = { path = "../leaper-power", package = "leaper-power" } runner = { path = "../leaper-runner", package = "leaper-runner" } +lock = { path = "../leaper-lock", package = "leaper-lock" } leaper-tracing.path = "../leaper-tracing" tracing.workspace = true diff --git a/leaper/src/app.rs b/leaper/src/app.rs deleted file mode 100644 index 8b13789..0000000 --- a/leaper/src/app.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/leaper/src/cli.rs b/leaper/src/cli.rs index ddadb04..0439e1f 100644 --- a/leaper/src/cli.rs +++ b/leaper/src/cli.rs @@ -21,4 +21,5 @@ pub enum AppMode { Launcher, Runner, Power, + Lock, } diff --git a/leaper/src/main.rs b/leaper/src/main.rs index 8bc8fac..ee6a7e4 100644 --- a/leaper/src/main.rs +++ b/leaper/src/main.rs @@ -1,9 +1,8 @@ -mod app; mod cli; use clap::Parser; use color_eyre::Result; -use mode::LeaperMode; +use mode::{LeaperMode, LeaperModeMultiWindow}; fn main() -> Result<()> { use crate::cli::Cli; @@ -23,6 +22,7 @@ fn main() -> Result<()> { cli::AppMode::Launcher => launcher::LeaperLauncher::run()?, cli::AppMode::Runner => runner::LeaperRunner::run()?, cli::AppMode::Power => power::LeaperPower::run()?, + cli::AppMode::Lock => lock::LeaperLock::run()?, } Ok(()) From cb5d60a5c7c67ef6dc30fd1f06a58944520b39ef Mon Sep 17 00:00:00 2001 From: Vitalii Lukyanov Date: Fri, 7 Nov 2025 23:04:17 +0100 Subject: [PATCH 2/2] clippy check --- leaper-lock/src/lib.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/leaper-lock/src/lib.rs b/leaper-lock/src/lib.rs index 8547a9a..6639e49 100644 --- a/leaper-lock/src/lib.rs +++ b/leaper-lock/src/lib.rs @@ -128,13 +128,15 @@ impl LeaperModeMultiWindow for LeaperLock { return Self::Task::done(LeaperLockMsg::UnLock); } - LeaperLockMsg::IcedEvent(ev) => match ev { - iced::Event::Keyboard(keyboard::Event::KeyPressed { + LeaperLockMsg::IcedEvent(ev) => { + if let iced::Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Enter), .. - }) => return Self::Task::done(Self::Msg::ConfirmPassword), - _ => {} - }, + }) = ev + { + return Self::Task::done(Self::Msg::ConfirmPassword); + } + } LeaperLockMsg::UnLock => return Self::Task::done(msg), }