Lines
91.49 %
Functions
47.06 %
Branches
100 %
//! Application state for the TUI.
//!
//! The TUI is organised as a small state machine:
//! - A top-level tab row decides which tab body is rendered.
//! - Each tab body is a multi-pane area managed by the tab itself.
//! - An overlay stack sits on top of the whole lot and intercepts input
//! when non-empty.
//! - A bottom command line is always visible.
//! All of this is pure state: no rendering happens in this file. The
//! draw layer reads from `App` and renders; the event layer mutates
//! `App` via named methods so tests can drive state transitions
//! without a real terminal.
mod convert;
mod edit;
mod eval;
mod form_options;
mod mutation;
use std::collections::HashMap;
use crate::overlay::OverlayStack;
use crate::route::Route;
use crate::tabs::config::ConfigTab;
use crate::tabs::list::ListTab;
use crate::tabs::nms::ConsoleState;
use crate::tabs::nms_eval::ConsoleEval;
use crate::tabs::reports::ReportsTab;
use crate::view::{Tab, ViewId, ViewMut, ViewRef};
use crate::widgets::{EditMode, Editor};
use cli_core::render::schema;
use plotting::ChartSpec;
use sqlx::types::Uuid;
/// Command-line palette editor state.
pub struct CmdLine {
pub editor: Editor,
pub active: bool,
/// Completion candidates for the current buffer, refreshed on each Tab press.
pub completions: Vec<String>,
}
impl CmdLine {
fn new(edit_mode: EditMode) -> Self {
Self {
editor: Editor::new(edit_mode),
active: false,
completions: Vec::new(),
pub struct App {
pub user_id: Uuid,
pub active_tab: Tab,
pub overlays: OverlayStack,
pub cmdline: CmdLine,
pub edit_mode: EditMode,
pub status: String,
pub should_quit: bool,
/// Whether the console input prompt has keyboard focus.
pub console_focused: bool,
/// The async eval bridge. `None` until attached via [`App::attach_console`].
console_eval: Option<ConsoleEval>,
/// Chart spec the active tab wants the runtime to emit as kitty graphics.
pending_chart: Option<ChartSpec>,
/// Routes pending eval reply ids to their destination view.
pending_routes: HashMap<i64, Route>,
/// Monotonic counter for form-options fetches; shared across all forms
/// that populate Selects. A stale reply (wrong seq) is silently dropped.
form_options_seq: u64,
pub accounts: ListTab,
pub transactions: ListTab,
pub commodities: ListTab,
pub reports: ReportsTab,
pub config: ConfigTab,
pub console: ConsoleState,
impl App {
#[must_use]
pub fn new(user_id: Uuid, edit_mode: EditMode) -> Self {
user_id,
active_tab: Tab::Reports,
overlays: OverlayStack::new(),
cmdline: CmdLine::new(edit_mode),
edit_mode,
status: String::new(),
should_quit: false,
console_focused: false,
console_eval: None,
pending_chart: None,
pending_routes: HashMap::new(),
form_options_seq: 0,
accounts: ListTab::new("(list-accounts)", &schema::ACCOUNTS),
transactions: ListTab::new("(list-transactions \"\")", &schema::TRANSACTIONS),
commodities: ListTab::new("(list-commodities)", &schema::COMMODITIES),
reports: ReportsTab::new(),
config: ConfigTab::new(),
console: ConsoleState::new(),
pub fn active_view(&self) -> ViewRef<'_> {
match self.active_tab {
ViewId::Accounts => ViewRef::List(ViewId::Accounts, &self.accounts),
ViewId::Transactions => ViewRef::List(ViewId::Transactions, &self.transactions),
ViewId::Commodities => ViewRef::List(ViewId::Commodities, &self.commodities),
ViewId::Reports => ViewRef::Reports(&self.reports),
ViewId::Config => ViewRef::Config(&self.config),
ViewId::Console => ViewRef::Console(&self.console),
pub fn active_view_mut(&mut self) -> ViewMut<'_> {
ViewId::Accounts => ViewMut::List(ViewId::Accounts, &mut self.accounts),
ViewId::Transactions => ViewMut::List(ViewId::Transactions, &mut self.transactions),
ViewId::Commodities => ViewMut::List(ViewId::Commodities, &mut self.commodities),
ViewId::Reports => ViewMut::Reports(&mut self.reports),
ViewId::Config => ViewMut::Config(&mut self.config),
ViewId::Console => ViewMut::Console(&mut self.console),
pub fn queue_chart(&mut self, spec: ChartSpec) {
self.pending_chart = Some(spec);
pub fn take_pending_chart(&mut self) -> Option<ChartSpec> {
self.pending_chart.take()
fn activate_tab(&mut self, tab: Tab) {
if tab != Tab::Console {
self.console_focused = false;
self.active_tab = tab;
self.ensure_tab_loaded(tab);
pub fn next_tab(&mut self) {
let idx = self.active_tab.index();
self.activate_tab(Tab::ALL[(idx + 1) % Tab::ALL.len()]);
pub fn previous_tab(&mut self) {
let len = Tab::ALL.len();
self.activate_tab(Tab::ALL[(idx + len - 1) % len]);
pub fn switch_tab(&mut self, tab: Tab) {
self.activate_tab(tab);
pub fn open_command_line(&mut self) {
self.cmdline.editor = Editor::new(self.edit_mode);
self.cmdline.active = true;
self.cmdline.completions.clear();
pub fn close_command_line(&mut self) {
self.cmdline.active = false;
pub fn set_status(&mut self, msg: impl Into<String>) {
self.status = msg.into();
pub fn request_quit(&mut self) {
self.should_quit = true;
pub fn set_edit_mode(&mut self, mode: EditMode) {
self.edit_mode = mode;
self.cmdline.editor.set_mode(mode);
pub fn attach_console(&mut self, eval: ConsoleEval) {
self.console_eval = Some(eval);
#[cfg(test)]
mod tests;