This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-tui/src/lib.rs
T
Miguel PalhasandClaude Opus 5 d39fbdbbfc feat(maestro): drop retained worktrees and header debug toggle
Removes the retained-worktree store end to end: the daemon's state-dir
store, GET/DELETE /api/worktrees, `maestro worktrees list|reclaim`, the
TUI overlay behind `w`, and the web `#/worktrees` view and header link.

The kill guard stays. A forced kill and a dirty natural exit now leave
the worktree on disk untouched instead of parking it in a store, so
removing one is an ordinary `git worktree remove`. A pristine tree is
still removed on exit. DELETE /api/sessions/:id always answers 204.

The header's debug button also goes; Ctrl+Shift+D and `?hud` still
toggle the HUD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 09:00:52 +01:00

139 lines
4.8 KiB
Rust

//! `maestro tui` — terminal UI client over the local daemon's `/api/*` surface.
#![deny(missing_debug_implementations)]
pub mod action;
pub mod app;
pub mod catalog;
pub mod cli;
pub mod config;
pub mod draft;
pub mod emulators;
pub mod fast_forward;
pub mod input;
pub mod keymap;
pub mod overlays;
pub mod palette;
pub mod pane;
pub mod projects;
pub mod render;
pub mod session_task;
pub mod sidebar;
pub mod state;
pub mod terminal;
pub mod text_input;
use std::path::PathBuf;
use std::time::Duration;
use maestro_protocol::DaemonClient;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use crate::app::App;
use crate::catalog::CatalogStore;
use crate::config::TuiConfig;
/// Inputs for [`run`]. Built by the umbrella binary from the parsed
/// `TuiArgs` plus any global flags.
#[derive(Debug, Clone)]
pub struct RunConfig {
/// HTTP base URL of the local daemon, e.g. `http://127.0.0.1:7070`.
/// No trailing slash.
pub daemon_url: String,
/// Explicit `--config <path>` override from the umbrella binary's global
/// flag. When `None`, [`maestro_config::default_config_path`] picks the
/// default (`$MAESTRO_CONFIG` or `~/.config/maestro/config.toml`).
pub config_path: Option<PathBuf>,
}
#[derive(Debug, thiserror::Error)]
pub enum TuiError {
#[error("daemon unreachable at {0}; check `systemctl --user status maestro-daemon`")]
DaemonUnreachable(String),
#[error("config: {0}")]
Config(String),
#[error("catalog: {0}")]
Catalog(String),
#[error("terminal: {0}")]
Terminal(String),
}
/// Entry point. Spawns the catalog WS loop, installs the panic hook, enters
/// the TUI alt screen, drives [`App::run`] until the user quits.
pub async fn run(cfg: RunConfig) -> Result<(), TuiError> {
terminal::install_panic_hook();
let base_url = cfg.daemon_url.trim_end_matches('/').to_string();
// Health check — fail fast if no daemon is reachable.
health_check(&DaemonClient::with_timeouts(
&base_url,
HEALTH_TIMEOUT,
HEALTH_TIMEOUT,
))
.await?;
let daemon = DaemonClient::with_timeouts(&base_url, REQUEST_TIMEOUT, REQUEST_TIMEOUT);
// Load runtime config: explicit `--config <path>` wins, then
// `$MAESTRO_CONFIG`, then the platform default. Missing file is fine —
// we fall back to shipped defaults.
let cfg_path = cfg.config_path.clone().map(Ok).unwrap_or_else(|| {
maestro_config::default_config_path().map_err(|e| TuiError::Config(e.to_string()))
});
let cfg_file = match cfg_path {
Ok(path) => maestro_config::MaestroConfigFile::load(&path)
.map_err(|e| TuiError::Config(e.to_string()))?,
Err(_) => maestro_config::MaestroConfigFile::default(),
};
let tui_cfg = TuiConfig::load(&cfg_file.tui).map_err(|e| TuiError::Config(e.to_string()))?;
palette::set_active(tui_cfg.palette);
let catalog = CatalogStore::default();
let ws_url = daemon
.events_url()
.map_err(|e| TuiError::Config(e.to_string()))?;
let bg_catalog = catalog.clone();
tokio::spawn(async move { bg_catalog.run_events_loop(ws_url).await });
// Wait briefly for the bootstrap Snapshot to land. If it doesn't, we
// still enter the UI — the sidebar will populate when events flow.
let _ = tokio::time::timeout(Duration::from_secs(2), catalog.wait_for_initial_snapshot()).await;
let _guard = terminal::TerminalGuard::enter().map_err(|e| match e {
terminal::TerminalError::Io(io) => TuiError::Terminal(io.to_string()),
})?;
let backend = CrosstermBackend::new(std::io::stdout());
let mut terminal = Terminal::new(backend).map_err(|e| TuiError::Terminal(e.to_string()))?;
let app = App::new(tui_cfg, catalog, daemon);
app.run(&mut terminal).await
}
/// Startup probe budget, applied to connect *and* the whole request: the user is
/// staring at a terminal, so an unreachable daemon must fail in under a second.
const HEALTH_TIMEOUT: Duration = Duration::from_millis(500);
/// Budget for the TUI's own calls once it is up. Tighter than the client
/// default — a spawn or kill that stalls should surface as a toast, not hang
/// the interface.
const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
async fn health_check(daemon: &DaemonClient) -> Result<(), TuiError> {
for _ in 0..3 {
if let Ok(info) = daemon.daemon_info().await {
if !maestro_protocol::is_compatible(&info.version) {
tracing::warn!(
daemon = %info.version,
client = %maestro_protocol::PROTOCOL_VERSION,
"daemon protocol version differs from this client's",
);
}
return Ok(());
}
tokio::time::sleep(HEALTH_TIMEOUT).await;
}
Err(TuiError::DaemonUnreachable(daemon.base().to_string()))
}