// ABOUTME: Current user detection and avatar loading for the power menu. // ABOUTME: Retrieves user info from the system (pwd, AccountsService, ~/.face). use nix::unistd::{getuid, User as NixUser}; use std::path::{Path, PathBuf}; const DEFAULT_ACCOUNTSSERVICE_DIR: &str = "/var/lib/AccountsService/icons"; /// Represents the current user for the power menu. #[derive(Debug, Clone)] pub struct User { pub username: String, pub display_name: String, pub home: PathBuf, pub uid: u32, } /// Get the currently logged-in user's info from the system. pub fn get_current_user() -> Option { let uid = getuid(); let nix_user = NixUser::from_uid(uid).ok()??; let gecos = nix_user.gecos.to_str().unwrap_or("").to_string(); // GECOS field may contain comma-separated values; first field is the full name let display_name = if !gecos.is_empty() { let first = gecos.split(',').next().unwrap_or(""); if first.is_empty() { nix_user.name.clone() } else { first.to_string() } } else { nix_user.name.clone() }; Some(User { username: nix_user.name, display_name, home: nix_user.dir, uid: uid.as_raw(), }) } /// Find the user's avatar image, checking ~/.face then AccountsService. pub fn get_avatar_path(home: &Path, username: Option<&str>) -> Option { get_avatar_path_with(home, username, Path::new(DEFAULT_ACCOUNTSSERVICE_DIR)) } /// Find avatar with configurable AccountsService dir (for testing). pub fn get_avatar_path_with( home: &Path, username: Option<&str>, accountsservice_dir: &Path, ) -> Option { // ~/.face takes priority — canonicalize to resolve symlinks let face = home.join(".face"); if face.exists() { if let Ok(canonical) = std::fs::canonicalize(&face) { log::debug!("Avatar: using ~/.face ({})", canonical.display()); return Some(canonical); } // canonicalize failed (e.g. permissions) — skip rather than return unresolved symlink log::warn!("Avatar: ~/.face exists but canonicalize failed, skipping"); } // AccountsService icon — also canonicalize for consistency if let Some(name) = username { if accountsservice_dir.exists() { let icon = accountsservice_dir.join(name); if icon.exists() { if let Ok(canonical) = std::fs::canonicalize(&icon) { log::debug!("Avatar: using AccountsService icon ({})", canonical.display()); return Some(canonical); } log::warn!("Avatar: AccountsService icon exists but canonicalize failed, skipping"); } } } None } /// Return the GResource path to the default avatar SVG. pub fn get_default_avatar_path() -> String { let prefix = crate::GRESOURCE_PREFIX; format!("{prefix}/default-avatar.svg") } #[cfg(test)] mod tests { use super::*; use std::fs; #[test] fn get_current_user_returns_some() { let user = get_current_user(); assert!(user.is_some()); let user = user.unwrap(); assert!(!user.username.is_empty()); assert!(!user.display_name.is_empty()); assert!(user.home.exists()); } #[test] fn returns_face_file_if_exists() { let dir = tempfile::tempdir().unwrap(); let face = dir.path().join(".face"); fs::write(&face, "fake image").unwrap(); let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent")); let expected = fs::canonicalize(&face).unwrap(); assert_eq!(path, Some(expected)); } #[test] fn returns_accountsservice_icon_if_exists() { let dir = tempfile::tempdir().unwrap(); let icons_dir = dir.path().join("icons"); fs::create_dir(&icons_dir).unwrap(); let icon = icons_dir.join("testuser"); fs::write(&icon, "fake image").unwrap(); let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir); let expected = fs::canonicalize(&icon).unwrap(); assert_eq!(path, Some(expected)); } #[test] fn face_file_takes_priority_over_accountsservice() { let dir = tempfile::tempdir().unwrap(); let face = dir.path().join(".face"); fs::write(&face, "fake image").unwrap(); let icons_dir = dir.path().join("icons"); fs::create_dir(&icons_dir).unwrap(); let icon = icons_dir.join("testuser"); fs::write(&icon, "fake image").unwrap(); let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir); let expected = fs::canonicalize(&face).unwrap(); assert_eq!(path, Some(expected)); } #[test] fn returns_none_when_no_avatar() { let dir = tempfile::tempdir().unwrap(); let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent")); assert!(path.is_none()); } #[test] fn default_avatar_path_is_gresource() { let path = get_default_avatar_path(); assert!(path.contains("default-avatar.svg")); assert!(path.starts_with("/dev/moonarch/moonset")); } }