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

65
apps/tui/src/runtime.rs Normal file
View File

@@ -0,0 +1,65 @@
//! Small in-process executor for storage calls. It never launches a process.
use std::{
sync::mpsc::{self, Receiver, Sender},
thread,
};
use crate::app::{AsyncPayload, AsyncResult, RequestToken};
pub struct AsyncExecutor {
sender: Sender<AsyncResult>,
receiver: Receiver<AsyncResult>,
}
impl Default for AsyncExecutor {
fn default() -> Self {
Self::new()
}
}
impl AsyncExecutor {
pub fn new() -> Self {
let (sender, receiver) = mpsc::channel();
Self { sender, receiver }
}
pub fn submit<F>(&self, token: RequestToken, work: F)
where
F: FnOnce() -> Result<AsyncPayload, String> + Send + 'static,
{
let sender = self.sender.clone();
thread::spawn(move || {
let _ignored = sender.send(AsyncResult {
token,
payload: work(),
});
});
}
pub fn drain(&self) -> impl Iterator<Item = AsyncResult> + '_ {
self.receiver.try_iter()
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::app::{App, ResultDisposition};
#[test]
fn work_completes_off_the_input_thread() {
let executor = AsyncExecutor::new();
let mut app = App::new();
let token = app.begin_request();
executor.submit(token, || Ok(AsyncPayload::Refreshed));
let result = executor
.receiver
.recv_timeout(Duration::from_secs(2))
.expect("worker should return a typed result");
assert_eq!(app.apply_result(result), ResultDisposition::Applied);
}
}