Move login state, authentication, discovery, input, rendering, and tests into focused TUI modules. Add detailed CLI error reports with recovery commands, align the build and host-check documentation, and keep the legacy Go tree outside normal builds. Checks: make check; make integration-test; go test ./... (legacy/go).
173 lines
6.3 KiB
Rust
173 lines
6.3 KiB
Rust
use std::sync::mpsc;
|
|
use std::thread;
|
|
|
|
use latchd_core::{accounts, preferences, session, session::SessionDescriptor};
|
|
|
|
use super::{App, NoticeLevel, unknown_account_status};
|
|
|
|
impl App {
|
|
pub(super) fn apply_preferred_session(&mut self) {
|
|
if let Some(index) = self
|
|
.preferences
|
|
.preferred_index(&self.username, &self.sessions)
|
|
{
|
|
self.selected = index;
|
|
}
|
|
}
|
|
|
|
pub(super) 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");
|
|
}
|
|
}
|
|
|
|
pub(super) fn poll_faillock(&mut self) {
|
|
let Some(receiver) = &self.faillock_receiver else {
|
|
return;
|
|
};
|
|
match receiver.try_recv() {
|
|
Ok(Ok(status)) => {
|
|
self.faillock_status = Some(status);
|
|
self.faillock_receiver = None;
|
|
if status.available {
|
|
if status.locked_out {
|
|
self.notify(
|
|
"Account has reached the advisory faillock threshold",
|
|
NoticeLevel::Warning,
|
|
);
|
|
} else if status.failures > 0 {
|
|
self.notify(
|
|
&format!("{} failed attempt(s) recorded", status.failures),
|
|
NoticeLevel::Info,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(Err(error)) => {
|
|
tracing::debug!(%error, "faillock query failed");
|
|
self.faillock_receiver = None;
|
|
}
|
|
Err(mpsc::TryRecvError::Empty) => {}
|
|
Err(mpsc::TryRecvError::Disconnected) => self.faillock_receiver = None,
|
|
}
|
|
}
|
|
|
|
pub(super) fn poll_refresh(&mut self) {
|
|
let Some(receiver) = &self.refresh_receiver else {
|
|
return;
|
|
};
|
|
match receiver.try_recv() {
|
|
Ok((sessions, accounts, account_statuses)) => {
|
|
self.sessions = sessions;
|
|
self.accounts = accounts;
|
|
self.account_statuses = account_statuses;
|
|
self.refresh_receiver = None;
|
|
self.refreshing = false;
|
|
self.reconcile_selection();
|
|
self.status = format!(
|
|
"Discovery refreshed: {} session(s), {} account(s)",
|
|
self.sessions.len(),
|
|
self.accounts.len()
|
|
);
|
|
self.notify("Account and session discovery refreshed", NoticeLevel::Info);
|
|
}
|
|
Err(mpsc::TryRecvError::Empty) => {}
|
|
Err(mpsc::TryRecvError::Disconnected) => {
|
|
self.refresh_receiver = None;
|
|
self.refreshing = false;
|
|
self.status = "Discovery refresh failed".into();
|
|
self.notify(
|
|
"Could not refresh account and session discovery",
|
|
NoticeLevel::Warning,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn start_refresh(&mut self) {
|
|
if self.refreshing {
|
|
self.status = "Discovery refresh is already running".into();
|
|
return;
|
|
}
|
|
let config = self.config.clone();
|
|
let (sender, receiver) = mpsc::sync_channel(1);
|
|
let task = thread::Builder::new()
|
|
.name("latchd-discovery".into())
|
|
.spawn(move || {
|
|
let sessions = session::discover(&config);
|
|
let (accounts, account_statuses) = if config.accounts.enumerate {
|
|
let records = accounts::discover_with_status(&config.accounts);
|
|
records
|
|
.into_iter()
|
|
.map(|record| (record.identity, record.status))
|
|
.unzip()
|
|
} else {
|
|
(Vec::new(), Vec::new())
|
|
};
|
|
let _ = sender.send((sessions, accounts, account_statuses));
|
|
});
|
|
match task {
|
|
Ok(_) => {
|
|
self.refresh_receiver = Some(receiver);
|
|
self.refreshing = true;
|
|
self.status = "Refreshing accounts and sessions…".into();
|
|
}
|
|
Err(error) => {
|
|
tracing::debug!(%error, "could not start discovery refresh");
|
|
self.status = format!("Could not start discovery refresh: {error}");
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn reconcile_selection(&mut self) {
|
|
if self.sessions.is_empty() {
|
|
self.selected = 0;
|
|
} else if self.selected >= self.sessions.len() {
|
|
self.selected = self.sessions.len() - 1;
|
|
}
|
|
if self.accounts.is_empty() {
|
|
self.account_index = None;
|
|
self.account_statuses.clear();
|
|
} else if self.account_statuses.len() != self.accounts.len() {
|
|
self.account_statuses
|
|
.resize(self.accounts.len(), unknown_account_status());
|
|
} else if self
|
|
.account_index
|
|
.is_some_and(|index| index >= self.accounts.len())
|
|
{
|
|
self.account_index = Some(0);
|
|
}
|
|
if let Some(index) = self.account_index {
|
|
if let Some(account) = self.accounts.get(index) {
|
|
self.username = account.username.clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn start_faillock_query(&mut self) {
|
|
let username = self.username.clone();
|
|
if username.is_empty() {
|
|
self.faillock_receiver = None;
|
|
self.faillock_status = None;
|
|
return;
|
|
}
|
|
let config = self.config.accounts.clone();
|
|
let (sender, receiver) = mpsc::sync_channel(1);
|
|
self.faillock_receiver = Some(receiver);
|
|
let task = thread::Builder::new()
|
|
.name("latchd-faillock".into())
|
|
.spawn(move || {
|
|
let result = accounts::faillock_status(&username, &config);
|
|
let _ = sender.send(result);
|
|
});
|
|
if let Err(error) = task {
|
|
tracing::debug!(%error, "could not start faillock query");
|
|
self.faillock_receiver = None;
|
|
}
|
|
}
|
|
}
|