diff --git a/Cargo.lock b/Cargo.lock index b8d49ec8c..75b42b6e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -1648,6 +1648,7 @@ dependencies = [ "eyre", "fixed_decimal", "fontdb 0.16.2", + "fprint-zbus", "freedesktop-desktop-entry", "futures", "hostname-validator", @@ -2608,6 +2609,14 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fprint-zbus" +version = "0.1.0" +source = "git+https://github.com/TitouanReal/dbus-settings-bindings?branch=fprint-support#ae1d0b47f18f2966fa27ef6d91b8fc98a98b2e84" +dependencies = [ + "zbus 4.4.0", +] + [[package]] name = "freedesktop-desktop-entry" version = "0.7.5" diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index 5ef3ccfb7..7ea91440d 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -32,6 +32,7 @@ derive_setters = "0.1.6" dirs = "5.0.1" downcast-rs = "1.2.1" eyre = "0.6.12" +fprint-zbus = { git = "https://github.com/TitouanReal/dbus-settings-bindings", branch = "fprint-support", optional = true } freedesktop-desktop-entry = "0.7.5" futures = "0.3.30" hostname-validator = "1.1.1" @@ -133,7 +134,7 @@ page-networking = [ page-power = ["dep:upower_dbus", "dep:zbus"] page-region = ["dep:lichen-system", "dep:locale1"] page-sound = ["dep:cosmic-settings-subscriptions"] -page-users = ["dep:accounts-zbus"] +page-users = ["dep:accounts-zbus", "dep:fprint-zbus"] page-window-management = ["dep:cosmic-settings-config"] page-workspaces = ["dep:cosmic-comp-config"] diff --git a/cosmic-settings/src/pages/system/users/fprint.rs b/cosmic-settings/src/pages/system/users/fprint.rs new file mode 100644 index 000000000..de04038c3 --- /dev/null +++ b/cosmic-settings/src/pages/system/users/fprint.rs @@ -0,0 +1,255 @@ +// Copyright 2025 Titouan Real +// SPDX-License-Identifier: GPL-3.0-only + +use std::{process::Output, str::FromStr}; + +use fprint_zbus::{FprintDeviceProxy, FprintManagerProxy}; +use zbus::Connection; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FingerName { + Any, + LeftThumb, + LeftIndexFinger, + LeftMiddleFinger, + LeftRingFinger, + LeftLittleFinger, + RightThumb, + RightIndexFinger, + RightMiddleFinger, + RightRingFinger, + RightLittleFinger, +} + +impl FingerName { + pub const ALL: &[FingerName] = &[ + Self::LeftThumb, + Self::LeftIndexFinger, + Self::LeftMiddleFinger, + Self::LeftRingFinger, + Self::LeftLittleFinger, + Self::RightThumb, + Self::RightIndexFinger, + Self::RightMiddleFinger, + Self::RightRingFinger, + Self::RightLittleFinger, + ]; +} + +impl FromStr for FingerName { + type Err = (); + + fn from_str(finger: &str) -> Result { + match finger.as_ref() { + "any" => Ok(Self::Any), + "left-thumb" => Ok(Self::LeftThumb), + "left-index-finger" => Ok(Self::LeftIndexFinger), + "left-middle-finger" => Ok(Self::LeftMiddleFinger), + "left-ring-finger" => Ok(Self::LeftRingFinger), + "left-little-finger" => Ok(Self::LeftLittleFinger), + "right-thumb" => Ok(Self::RightThumb), + "right-index-finger" => Ok(Self::RightIndexFinger), + "right-middle-finger" => Ok(Self::RightMiddleFinger), + "right-ring-finger" => Ok(Self::RightRingFinger), + "right-little-finger" => Ok(Self::RightLittleFinger), + other => Err(()), + } + } +} + +impl ToString for FingerName { + fn to_string(&self) -> String { + match self { + Self::Any => fl!("finger", "any"), + Self::LeftThumb => fl!("finger", "left-thumb"), + Self::LeftIndexFinger => fl!("finger", "left-index-finger"), + Self::LeftMiddleFinger => fl!("finger", "left-middle-finger"), + Self::LeftRingFinger => fl!("finger", "left-ring-finger"), + Self::LeftLittleFinger => fl!("finger", "left-little-finger"), + Self::RightThumb => fl!("finger", "right-thumb"), + Self::RightIndexFinger => fl!("finger", "right-index-finger"), + Self::RightMiddleFinger => fl!("finger", "right-middle-finger"), + Self::RightRingFinger => fl!("finger", "right-ring-finger"), + Self::RightLittleFinger => fl!("finger", "right-little-finger"), + } + .to_string() + } +} + +#[derive(Clone, Debug, Default)] +pub struct FprintDeviceInfo { + pub name: String, + pub path: zbus::zvariant::OwnedObjectPath, + pub enrolled_fingers: Vec, + pub ongoing_enroll: bool, +} + +#[derive(Clone, Debug, Default)] +pub struct FprintInfo { + pub default_device: Option, + pub other_devices: Vec, +} + +pub async fn start_enroll_finger( + device: zbus::zvariant::OwnedObjectPath, + username: &str, +) -> Result<(), ()> { + let daemon = get_fprint_device_proxy(&device).await?; + + if let Err(e) = daemon.claim(username).await { + tracing::error!("{e}"); + return Err(()); + } + + Ok(()) +} + +pub async fn stop_enroll_finger( + device: zbus::zvariant::OwnedObjectPath, + username: &str, +) -> Result<(), ()> { + let daemon = get_fprint_device_proxy(&device).await?; + + if let Err(e) = daemon.release().await { + tracing::error!("{e}"); + return Err(()); + } + + Ok(()) +} + +async fn get_fprint_device_info( + object_path: zbus::zvariant::OwnedObjectPath, + username: &str, +) -> Result { + let daemon = get_fprint_device_proxy(&object_path).await?; + let name = match daemon.name().await { + Ok(name) => name, + Err(e) => { + tracing::error!("{e}"); + return Err(()); + } + }; + + let fingers = match daemon.list_enrolled_fingers(username).await { + Ok(fingers) => { + let mut fingers_vec = Vec::new(); + for finger in fingers { + fingers_vec.push(match finger.parse() { + Ok(FingerName::Any) | Err(_) => { + tracing::error!("Received unexpected finger name: {finger}"); + return Err(()); + } + Ok(finger) => finger, + }); + } + fingers_vec + } + Err(e) => match e.to_string().as_str() { + "net.reactivated.Fprint.Error.NoEnrolledPrints: Failed to discover prints" => { + Vec::new() + } + _ => { + tracing::error!("{e}"); + return Err(()); + } + }, + }; + + Ok(FprintDeviceInfo { + name, + path: object_path, + enrolled_fingers: fingers, + ongoing_enroll: false, + }) +} + +pub async fn get_fprint_info(username: &str) -> Result { + let daemon = get_fprint_manager_proxy().await?; + + let default_device_path = match daemon.get_default_device().await { + Ok(device) => Some(device), + Err(zbus::Error::MethodError(_, _, _)) => None, + Err(e) => { + tracing::error!("{e}"); + return Err(()); + } + }; + + let default_device_info = match default_device_path { + Some(ref path) => Some(get_fprint_device_info(path.clone(), username).await?), + None => None, + }; + + tracing::info!("Default device: {:?}", default_device_info); + + let other_devices = match daemon.get_devices().await { + Ok(mut devices) => { + if let Some(default_device) = default_device_path { + devices.retain(|device| device != &default_device); + } + let mut devices_info = Vec::new(); + for device in devices { + devices_info.push(get_fprint_device_info(device, username).await?) + } + devices_info + } + Err(e) => { + tracing::error!("{e}"); + return Err(()); + } + }; + + tracing::info!("Other devices: {:?}", other_devices); + + Ok(FprintInfo { + default_device: default_device_info, + other_devices, + }) +} + +async fn get_fprint_manager_proxy<'a>() -> Result, ()> { + let connection = match Connection::system().await { + Ok(c) => c, + Err(e) => { + tracing::error!("zbus connection failed. {e}"); + return Err(()); + } + }; + + match FprintManagerProxy::new(&connection).await { + Ok(d) => Ok(d), + Err(e) => { + tracing::error!("Fprint daemon proxy can't be created. Is it installed? {e}"); + Err(()) + } + } +} + +async fn get_fprint_device_proxy<'a>( + device_object_path: &'a zbus::zvariant::OwnedObjectPath, +) -> Result, ()> { + let connection = match Connection::system().await { + Ok(c) => c, + Err(e) => { + tracing::error!("zbus connection failed. {e}"); + return Err(()); + } + }; + + let proxy_builder = match FprintDeviceProxy::builder(&connection).path(device_object_path) { + Ok(d) => d, + Err(e) => { + tracing::error!("{e}"); + return Err(()); + } + }; + + match proxy_builder.build().await { + Ok(d) => Ok(d), + Err(e) => { + tracing::error!("Fprint daemon proxy can't be created. Is it installed? {e}"); + Err(()) + } + } +} diff --git a/cosmic-settings/src/pages/system/users/mod.rs b/cosmic-settings/src/pages/system/users/mod.rs index ce1791418..c35d5aa80 100644 --- a/cosmic-settings/src/pages/system/users/mod.rs +++ b/cosmic-settings/src/pages/system/users/mod.rs @@ -1,6 +1,7 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only +mod fprint; mod getent; use cosmic::{ @@ -10,6 +11,7 @@ use cosmic::{ Apply, Element, }; use cosmic_settings_page::{self as page, section, Section}; +use fprint::{FingerName, FprintInfo}; use slab::Slab; use slotmap::SlotMap; use std::{ @@ -37,6 +39,7 @@ pub struct User { full_name_edit: bool, password_edit: bool, username_edit: bool, + fingerprint_info: FprintInfo, is_admin: bool, } @@ -50,6 +53,7 @@ pub enum EditorField { #[derive(Clone, Debug)] pub enum Dialog { AddNewUser(User), + Fingerprint(zbus::zvariant::OwnedObjectPath, User, Vec), } #[derive(Clone, Debug)] @@ -98,6 +102,7 @@ pub enum Message { SelectUser(usize), SelectedUserDelete(u64), SelectedUserSetAdmin(u64, bool), + StartEnrollFinger(zbus::zvariant::OwnedObjectPath, User), ToggleEdit(usize, EditorField), } @@ -217,6 +222,30 @@ impl page::Page for Page { .secondary_action(cancel_button) .apply(Element::from) } + Dialog::Fingerprint(device, user, enrolled_fingers) => { + let mut control = widget::ListColumn::default(); + for finger in FingerName::ALL { + control = control.add(settings::item( + finger.to_string(), + if enrolled_fingers.contains(&finger) { + widget::text(fl!("registered")).apply(Element::from) + } else { + widget::button::standard(fl!("register")) + // .on_press(Message::StartEnrollFinger(device.clone(), user.clone())) + .apply(Element::from) + }, + )) + } + + let cancel_button = + widget::button::standard(fl!("close")).on_press(Message::Dialog(None)); + + widget::dialog() + .title(fl!("fingerprints")) + .control(control) + .secondary_action(cancel_button) + .apply(Element::from) + } }; dialog_element.map(crate::pages::Message::User).into() @@ -278,6 +307,13 @@ impl Page { admin_group.map_or(false, |group| group.users.contains(&user.username)) } }, + fingerprint_info: match fprint::get_fprint_info(&user.username).await { + Ok(info) => info, + Err(_) => { + tracing::error!("Failed to get fprint info"); + FprintInfo::default() + } + }, username: String::from(user.username), full_name: String::from(user.full_name), password: String::new(), @@ -567,6 +603,10 @@ impl Page { Message::ChangedAccountType(uid, is_admin) }); } + + Message::StartEnrollFinger(device, user) => { + fprint::start_enroll_finger(device, &user.username); + } }; cosmic::Task::none() @@ -634,19 +674,40 @@ fn user_list() -> Section { let mut details_list = widget::list_column() .add(settings::item(&page.fullname_label, fullname)) .add(settings::item(&page.username_label, username)) - .add(settings::item(&page.password_label, password)) - .add(settings::item_row(vec![ - column::with_capacity(2) - .push(text::body(crate::fl!("administrator"))) - .push(text::caption(crate::fl!("administrator", "desc"))) - .into(), - widget::horizontal_space().width(Length::Fill).into(), - widget::toggler(user.is_admin) - .on_toggle(|enabled| { - Message::SelectedUserSetAdmin(user.id, enabled) - }) - .into(), - ])); + .add(settings::item(&page.password_label, password)); + + if let Some(device) = &user.fingerprint_info.default_device { + details_list = details_list.add(settings::item( + format!("{} (Default Device)", device.name.clone()), + widget::button::standard(fl!("see-fingerprints")).on_press( + Message::Dialog(Some(Dialog::Fingerprint( + device.path.clone(), + user.clone(), + device.enrolled_fingers.clone(), + ))), + ), + )); + } + + for device in &user.fingerprint_info.other_devices { + details_list = details_list.add(settings::item( + device.name.clone(), + widget::text(format!("{:?}", device.enrolled_fingers)), + )); + } + + details_list = details_list.add(settings::item_row(vec![ + column::with_capacity(2) + .push(text::body(crate::fl!("administrator"))) + .push(text::caption(crate::fl!("administrator", "desc"))) + .into(), + widget::horizontal_space().width(Length::Fill).into(), + widget::toggler(user.is_admin) + .on_toggle(|enabled| { + Message::SelectedUserSetAdmin(user.id, enabled) + }) + .into(), + ])); if page.users.len() > 1 { details_list = details_list.add(settings::item_row(vec![ diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index bf09e1c37..b0519f9d3 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -777,6 +777,8 @@ users = Users .standard = Standard .profile-add = Choose profile image +see-fingerprints = See fingerprints + administrator = Administrator .desc = Administrators can change settings for all users, add and remove other users. @@ -786,6 +788,24 @@ full-name = Full name username = Username password = Password +fingerprints = Fingerprints + +finger = Finger + .any = Any + .left-thumb = Left Thumb + .left-index-finger = Left Index Finger + .left-middle-finger = Left Middle Finger + .left-ring-finger = Left Ring Finger + .left-little-finger = Left Little Finger + .right-thumb = Right Thumb + .right-index-finger = Right Index Finger + .right-middle-finger = Right Middle Finger + .right-ring-finger = Right Ring Finger + .right-little-finger = Right Little Finger + +registered = Registered +register = Register + ## System: Default Applications default-apps = Default Applications diff --git a/i18n/fr/cosmic_settings.ftl b/i18n/fr/cosmic_settings.ftl index a1eb3e381..6ba2333f9 100644 --- a/i18n/fr/cosmic_settings.ftl +++ b/i18n/fr/cosmic_settings.ftl @@ -463,7 +463,7 @@ power-profiles = Modes d'énergie .no-backend = Backend non trouvé. Installez system76-power ou power-profiles-daemon. power-saving = Options d'économie d'énergie - .turn-off-screen-after = Éteindre l'écran après + .turn-off-screen-after = Éteindre l'écran après .auto-suspend = Suspension automatique .auto-suspend-ac = Suspension automatique lors du branchement .auto-suspend-battery = Suspension automatique sur batterie