Replace env_logger file-based logging with systemd-journal-logger for consistency with moonlock and native journalctl integration. Add debug-level logging at all decision points: config loading, user/session detection, avatar resolution, locale detection, IPC messages, login flow, and persistence. No credentials are ever logged.
242 lines
7.3 KiB
Rust
242 lines
7.3 KiB
Rust
// ABOUTME: Session detection — discovers available Wayland and X11 sessions.
|
|
// ABOUTME: Parses .desktop files from standard session directories.
|
|
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
const DEFAULT_WAYLAND_DIRS: &[&str] = &["/usr/share/wayland-sessions"];
|
|
const DEFAULT_XSESSION_DIRS: &[&str] = &["/usr/share/xsessions"];
|
|
|
|
/// Represents an available login session.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Session {
|
|
pub name: String,
|
|
pub exec_cmd: String,
|
|
pub session_type: String,
|
|
}
|
|
|
|
/// Parse a .desktop file and return a Session, or None if invalid.
|
|
fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
|
let content = fs::read_to_string(path).ok()?;
|
|
|
|
let mut in_section = false;
|
|
let mut name: Option<String> = None;
|
|
let mut exec_cmd: Option<String> = None;
|
|
|
|
for line in content.lines() {
|
|
let line = line.trim();
|
|
|
|
if line.starts_with('[') {
|
|
in_section = line == "[Desktop Entry]";
|
|
continue;
|
|
}
|
|
|
|
if !in_section {
|
|
continue;
|
|
}
|
|
|
|
if let Some(value) = line.strip_prefix("Name=") {
|
|
if name.is_none() {
|
|
name = Some(value.to_string());
|
|
}
|
|
} else if let Some(value) = line.strip_prefix("Exec=") {
|
|
if exec_cmd.is_none() {
|
|
exec_cmd = Some(value.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
let name = name.filter(|s| !s.is_empty());
|
|
let exec_cmd = exec_cmd.filter(|s| !s.is_empty());
|
|
|
|
if name.is_none() || exec_cmd.is_none() {
|
|
log::debug!("Skipping {}: missing Name={} Exec={}", path.display(),
|
|
name.is_some(), exec_cmd.is_some());
|
|
return None;
|
|
}
|
|
|
|
let name = name?;
|
|
let exec_cmd = exec_cmd?;
|
|
|
|
Some(Session {
|
|
name,
|
|
exec_cmd,
|
|
session_type: session_type.to_string(),
|
|
})
|
|
}
|
|
|
|
/// Discover available sessions from .desktop files.
|
|
pub fn get_sessions(
|
|
wayland_dirs: Option<&[PathBuf]>,
|
|
xsession_dirs: Option<&[PathBuf]>,
|
|
) -> Vec<Session> {
|
|
let default_wayland: Vec<PathBuf> =
|
|
DEFAULT_WAYLAND_DIRS.iter().map(PathBuf::from).collect();
|
|
let default_xsession: Vec<PathBuf> =
|
|
DEFAULT_XSESSION_DIRS.iter().map(PathBuf::from).collect();
|
|
|
|
let wayland = wayland_dirs.unwrap_or(&default_wayland);
|
|
let xsession = xsession_dirs.unwrap_or(&default_xsession);
|
|
|
|
let mut sessions = Vec::new();
|
|
|
|
for (dirs, session_type) in [(wayland, "wayland"), (xsession, "x11")] {
|
|
for directory in dirs {
|
|
let entries = match fs::read_dir(directory) {
|
|
Ok(e) => {
|
|
log::debug!("Scanning session directory: {}", directory.display());
|
|
e
|
|
}
|
|
Err(_) => continue,
|
|
};
|
|
|
|
let mut paths: Vec<PathBuf> = entries
|
|
.filter_map(|e| e.ok())
|
|
.map(|e| e.path())
|
|
.filter(|p| p.extension().is_some_and(|ext| ext == "desktop"))
|
|
.collect();
|
|
paths.sort();
|
|
|
|
for path in paths {
|
|
if let Some(session) = parse_desktop_file(&path, session_type) {
|
|
sessions.push(session);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log::debug!("Found {} session(s)", sessions.len());
|
|
sessions
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn write_desktop(dir: &Path, name: &str, content: &str) {
|
|
fs::write(dir.join(name), content).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn parse_valid_desktop_file() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("test.desktop");
|
|
fs::write(
|
|
&file,
|
|
"[Desktop Entry]\nName=Niri\nExec=niri-session\n",
|
|
)
|
|
.unwrap();
|
|
let session = parse_desktop_file(&file, "wayland").unwrap();
|
|
assert_eq!(session.name, "Niri");
|
|
assert_eq!(session.exec_cmd, "niri-session");
|
|
assert_eq!(session.session_type, "wayland");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_desktop_file_missing_name() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("test.desktop");
|
|
fs::write(&file, "[Desktop Entry]\nExec=niri-session\n").unwrap();
|
|
assert!(parse_desktop_file(&file, "wayland").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_desktop_file_missing_exec() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("test.desktop");
|
|
fs::write(&file, "[Desktop Entry]\nName=Niri\n").unwrap();
|
|
assert!(parse_desktop_file(&file, "wayland").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_desktop_file_wrong_section() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("test.desktop");
|
|
fs::write(
|
|
&file,
|
|
"[Other Section]\nName=Niri\nExec=niri-session\n",
|
|
)
|
|
.unwrap();
|
|
assert!(parse_desktop_file(&file, "wayland").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn get_sessions_finds_wayland_and_x11() {
|
|
let wayland_dir = tempfile::tempdir().unwrap();
|
|
let x11_dir = tempfile::tempdir().unwrap();
|
|
|
|
write_desktop(
|
|
wayland_dir.path(),
|
|
"niri.desktop",
|
|
"[Desktop Entry]\nName=Niri\nExec=niri-session\n",
|
|
);
|
|
write_desktop(
|
|
x11_dir.path(),
|
|
"i3.desktop",
|
|
"[Desktop Entry]\nName=i3\nExec=i3\n",
|
|
);
|
|
|
|
let wayland_paths = vec![wayland_dir.path().to_path_buf()];
|
|
let x11_paths = vec![x11_dir.path().to_path_buf()];
|
|
let sessions = get_sessions(Some(&wayland_paths), Some(&x11_paths));
|
|
|
|
assert_eq!(sessions.len(), 2);
|
|
assert_eq!(sessions[0].name, "Niri");
|
|
assert_eq!(sessions[0].session_type, "wayland");
|
|
assert_eq!(sessions[1].name, "i3");
|
|
assert_eq!(sessions[1].session_type, "x11");
|
|
}
|
|
|
|
#[test]
|
|
fn get_sessions_skips_missing_dirs() {
|
|
let sessions = get_sessions(
|
|
Some(&[PathBuf::from("/nonexistent")]),
|
|
Some(&[PathBuf::from("/also-nonexistent")]),
|
|
);
|
|
assert!(sessions.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn get_sessions_skips_invalid_files() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_desktop(
|
|
dir.path(),
|
|
"valid.desktop",
|
|
"[Desktop Entry]\nName=Valid\nExec=valid\n",
|
|
);
|
|
write_desktop(
|
|
dir.path(),
|
|
"invalid.desktop",
|
|
"[Desktop Entry]\nName=Invalid\n",
|
|
);
|
|
// Non-.desktop file
|
|
fs::write(dir.path().join("readme.txt"), "not a session").unwrap();
|
|
|
|
let paths = vec![dir.path().to_path_buf()];
|
|
let sessions = get_sessions(Some(&paths), Some(&[]));
|
|
assert_eq!(sessions.len(), 1);
|
|
assert_eq!(sessions[0].name, "Valid");
|
|
}
|
|
|
|
#[test]
|
|
fn sessions_sorted_alphabetically() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_desktop(
|
|
dir.path(),
|
|
"z-sway.desktop",
|
|
"[Desktop Entry]\nName=Sway\nExec=sway\n",
|
|
);
|
|
write_desktop(
|
|
dir.path(),
|
|
"a-niri.desktop",
|
|
"[Desktop Entry]\nName=Niri\nExec=niri-session\n",
|
|
);
|
|
|
|
let paths = vec![dir.path().to_path_buf()];
|
|
let sessions = get_sessions(Some(&paths), Some(&[]));
|
|
assert_eq!(sessions.len(), 2);
|
|
assert_eq!(sessions[0].name, "Niri");
|
|
assert_eq!(sessions[1].name, "Sway");
|
|
}
|
|
}
|