Build the Mutt-style TUI shell and action model

This commit is contained in:
Hermes Agent
2026-08-10 05:46:32 +00:00
parent f03fdc063c
commit 42e6a82b2d
7 changed files with 1051 additions and 42 deletions

64
apps/tui/src/lib.rs Normal file
View File

@@ -0,0 +1,64 @@
#![forbid(unsafe_code)]
#![deny(clippy::disallowed_types)]
//! Presentation-only state, rendering, and event plumbing for the terminal UI.
pub mod action;
pub mod app;
pub mod runtime;
pub mod terminal;
pub mod ui;
use std::{io, time::Duration};
use crossterm::event::{self, Event, KeyEventKind};
use ratatui::DefaultTerminal;
use crate::{
action::resolve_key,
app::{App, AsyncPayload, StartupSummary},
runtime::AsyncExecutor,
};
const TICK_INTERVAL: Duration = Duration::from_millis(250);
/// Run the interactive event loop. Polling keeps input responsive while storage
/// work runs on the executor and periodic repaints are pending.
pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
let mut app = App::new();
let executor = AsyncExecutor::new();
let startup = app.begin_request();
executor.submit(startup, || {
ironstorage::config::Config::load(None)
.map(|config| {
AsyncPayload::Startup(StartupSummary {
configuration: config.source().display().to_string(),
vault: config.vault().display().to_string(),
})
})
.map_err(|error| error.to_string())
});
while !app.should_quit() {
for result in executor.drain() {
app.apply_result(result);
}
terminal.draw(|frame| ui::draw(frame, &app))?;
if !event::poll(TICK_INTERVAL)? {
app.tick();
continue;
}
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if let Some(action) = resolve_key(app.mode(), key.code, key.modifiers) {
app.dispatch(action);
}
}
Event::Resize(width, height) => app.resize(width, height),
_ => {}
}
}
Ok(())
}