Files
latchd/crates/latchd-tui/src/app/discovery.rs
Zane Walker f2a885d621 refactor: split the TUI and refresh project docs
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).
2026-10-05 21:37:15 +05:30

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;
}
}
}