feat: complete session preferences and PAM fixture gates
This commit is contained in:
@@ -23,7 +23,6 @@ const MAX_ANIMATION_WIDTH: usize = 160;
|
||||
const MAX_ANIMATION_HEIGHT: usize = 64;
|
||||
const FRAME_INTERVAL: Duration = Duration::from_millis(120);
|
||||
|
||||
/// Background state owned by the TUI thread.
|
||||
pub(crate) struct Background {
|
||||
wallpaper: Option<RgbImage>,
|
||||
automata: Automata,
|
||||
@@ -34,7 +33,6 @@ pub(crate) struct Background {
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Loads a bounded wallpaper image and prepares the procedural layer.
|
||||
pub(crate) fn load(config: &VisualConfig) -> Result<Self, Error> {
|
||||
let wallpaper = config
|
||||
.wallpaper_path
|
||||
@@ -191,8 +189,7 @@ struct Automata {
|
||||
|
||||
impl Automata {
|
||||
fn resize(&mut self, width: usize, height: usize) {
|
||||
// Keep procedural state bounded even when a terminal reports an
|
||||
// unusually large window. Rendering remains clipped to this layer.
|
||||
// Cap the state size. Some terminal emulators report extreme dimensions.
|
||||
let width = width.min(MAX_ANIMATION_WIDTH);
|
||||
let height = height.min(MAX_ANIMATION_HEIGHT);
|
||||
if self.width == width && self.height == height {
|
||||
|
||||
@@ -21,6 +21,7 @@ use latchd_core::{
|
||||
audit::{AuditAction, AuditEvent},
|
||||
config::Config,
|
||||
error::{Error, ResultExt},
|
||||
preferences::{self, LastUsedPreferences},
|
||||
session::{self, SessionDescriptor},
|
||||
};
|
||||
|
||||
@@ -41,11 +42,8 @@ fn unknown_account_status() -> AccountStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of one completed login-screen interaction.
|
||||
pub struct LoginSelection {
|
||||
/// Authenticated PAM ticket.
|
||||
pub ticket: AuthTicket,
|
||||
/// Selected session descriptor.
|
||||
pub session: SessionDescriptor,
|
||||
}
|
||||
|
||||
@@ -130,6 +128,8 @@ struct App {
|
||||
refresh_receiver: Option<Receiver<DiscoverySnapshot>>,
|
||||
refreshing: bool,
|
||||
high_contrast: bool,
|
||||
preferences: LastUsedPreferences,
|
||||
preference_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -181,6 +181,8 @@ impl App {
|
||||
refresh_receiver: None,
|
||||
refreshing: false,
|
||||
high_contrast: std::env::var_os("LATCHD_HIGH_CONTRAST").is_some(),
|
||||
preferences: LastUsedPreferences::default(),
|
||||
preference_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +260,7 @@ impl App {
|
||||
.get(self.selected)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::Session("no session selected".into()))?;
|
||||
self.remember_selected_session(&selected);
|
||||
Ok(Some(LoginSelection {
|
||||
ticket,
|
||||
session: selected,
|
||||
@@ -334,6 +337,25 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_preferred_session(&mut self) {
|
||||
if let Some(index) = self
|
||||
.preferences
|
||||
.preferred_index(&self.username, &self.sessions)
|
||||
{
|
||||
self.selected = index;
|
||||
}
|
||||
}
|
||||
|
||||
fn remember_selected_session(&mut self, session: &SessionDescriptor) {
|
||||
self.preferences.remember(&self.username, session);
|
||||
let Some(path) = self.preference_path.clone() else {
|
||||
return;
|
||||
};
|
||||
if let Err(error) = preferences::save(&path, &self.preferences) {
|
||||
tracing::debug!(%error, "could not persist last-session preference");
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_faillock(&mut self) {
|
||||
let Some(receiver) = &self.faillock_receiver else {
|
||||
return;
|
||||
@@ -995,6 +1017,7 @@ impl App {
|
||||
self.auth_blocked_until = None;
|
||||
self.faillock_status = None;
|
||||
self.status = format!("Selected account {}", self.username);
|
||||
self.apply_preferred_session();
|
||||
self.start_faillock_query();
|
||||
self.notify(
|
||||
&format!("Selected account {}", self.username),
|
||||
@@ -1368,9 +1391,25 @@ pub fn run(
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
let background = Background::load(&config.visual)?;
|
||||
let preference_path = config
|
||||
.environment_switcher
|
||||
.remember_last_session
|
||||
.then(|| config.environment_switcher.last_session_path.clone());
|
||||
let preferences =
|
||||
preference_path
|
||||
.as_deref()
|
||||
.map_or_else(LastUsedPreferences::default, |path| {
|
||||
preferences::load(path).unwrap_or_else(|error| {
|
||||
tracing::debug!(%error, "could not load last-session preferences");
|
||||
LastUsedPreferences::default()
|
||||
})
|
||||
});
|
||||
let mut app = App::new(config, auth, preview, sessions, accounts, background);
|
||||
app.account_statuses = account_statuses;
|
||||
app.account_index = (!app.accounts.is_empty()).then_some(0);
|
||||
app.preferences = preferences;
|
||||
app.preference_path = preference_path;
|
||||
app.apply_preferred_session();
|
||||
app.start_faillock_query();
|
||||
ratatui::run(|terminal| app.run(terminal)).map_err(|error| Error::Terminal(error.to_string()))
|
||||
}
|
||||
@@ -1654,6 +1693,54 @@ mod tests {
|
||||
assert!(rendered.contains("New password"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_session_preference_follows_account_and_persists_success() {
|
||||
let auth = AuthService::new().expect("PAM worker should start");
|
||||
let sessions = vec![
|
||||
SessionDescriptor {
|
||||
kind: session::SessionKind::Tty,
|
||||
title: "TTY shell".into(),
|
||||
argv: Vec::new(),
|
||||
current_desktop: None,
|
||||
session_desktop: None,
|
||||
},
|
||||
SessionDescriptor {
|
||||
kind: session::SessionKind::Wayland,
|
||||
title: "Sway".into(),
|
||||
argv: vec!["sway".into()],
|
||||
current_desktop: None,
|
||||
session_desktop: None,
|
||||
},
|
||||
];
|
||||
let mut app = App::new(
|
||||
Config::default(),
|
||||
auth,
|
||||
true,
|
||||
sessions,
|
||||
vec![Identity {
|
||||
username: "alice".into(),
|
||||
uid: 1000,
|
||||
gid: 1000,
|
||||
home: "/home/alice".into(),
|
||||
shell: "/bin/sh".into(),
|
||||
}],
|
||||
Background::load(&Config::default().visual).expect("default background should load"),
|
||||
);
|
||||
app.preferences.remember("alice", &app.sessions[1]);
|
||||
app.apply_preferred_session();
|
||||
assert_eq!(app.selected, 1);
|
||||
|
||||
let directory =
|
||||
std::env::temp_dir().join(format!("latchd-tui-preferences-{}", std::process::id()));
|
||||
app.preference_path = Some(directory.join("last-session.toml"));
|
||||
let selected = app.sessions[1].clone();
|
||||
app.remember_selected_session(&selected);
|
||||
let loaded = preferences::load(&directory.join("last-session.toml"))
|
||||
.expect("selected session should persist");
|
||||
assert_eq!(loaded.preferred_index("alice", &app.sessions), Some(1));
|
||||
let _ = std::fs::remove_dir_all(directory);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtering_is_smart_case_and_selects_a_matching_session() {
|
||||
let auth = AuthService::new().expect("PAM worker should start");
|
||||
|
||||
Reference in New Issue
Block a user