generated from eblume/project-template
`o` on a task suspends the TUI, opens its canonical-context doc in the owner's
nvim via heph.nvim's live buffer surface (+lua require('heph.node').open), then
restores the alternate screen and reloads to pick up edits. The child nvim is
pointed at the same daemon via $HEPH_SOCKET, so it works under a custom
--socket too. This is the KB↔task fusion — edit the description/checklist in
the real editor and return straight to triage.
handle_key now returns an Action the event loop performs (the suspend/spawn is
terminal-owning, kept out of App). nvim arg builder unit-tested; the actual
suspend/spawn is interactive so it's exercised manually.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
133 lines
4.4 KiB
Rust
133 lines
4.4 KiB
Rust
//! `heph-tui` binary: terminal lifecycle + event loop. All state/logic lives in
|
|
//! the library ([`heph_tui::App`] + [`heph_tui::ui`]); this file is the I/O shell.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::{Context, Result};
|
|
use clap::Parser;
|
|
use heph_tui::{app::App, backend::ClientBackend, editor, ui, Focus};
|
|
use hephd::Client;
|
|
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
|
|
/// A key action that the event loop (which owns the terminal) must perform —
|
|
/// kept out of `App` so app logic stays terminal-free.
|
|
enum Action {
|
|
/// Suspend and open this node in nvim, then reload.
|
|
EditContext(String),
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "heph-tui", about = "Hephaestus task agenda / triage TUI")]
|
|
struct Cli {
|
|
/// Path to the hephd unix socket. Falls back to $HEPH_SOCKET, then the
|
|
/// standard runtime path.
|
|
#[arg(long)]
|
|
socket: Option<PathBuf>,
|
|
}
|
|
|
|
fn resolve_socket(flag: Option<PathBuf>) -> PathBuf {
|
|
flag.or_else(|| std::env::var_os("HEPH_SOCKET").map(PathBuf::from))
|
|
.unwrap_or_else(hephd::default_socket_path)
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
let socket = resolve_socket(cli.socket);
|
|
|
|
let client = Client::connect(&socket).with_context(|| {
|
|
format!(
|
|
"could not connect to hephd at {} — is it running? (try: heph daemon start)",
|
|
socket.display()
|
|
)
|
|
})?;
|
|
let app = App::new(ClientBackend::new(client)).context("loading the agenda")?;
|
|
|
|
let mut terminal = ratatui::init();
|
|
let result = run(&mut terminal, app, &socket);
|
|
ratatui::restore();
|
|
result
|
|
}
|
|
|
|
fn run<B: heph_tui::Backend>(
|
|
terminal: &mut ratatui::DefaultTerminal,
|
|
mut app: App<B>,
|
|
socket: &std::path::Path,
|
|
) -> Result<()> {
|
|
loop {
|
|
terminal.draw(|f| ui::render(f, &app))?;
|
|
if let Event::Key(key) = event::read()? {
|
|
if key.kind == KeyEventKind::Press {
|
|
if let Some(action) = handle_key(&mut app, key) {
|
|
perform(terminal, &mut app, socket, action)?;
|
|
}
|
|
}
|
|
}
|
|
if app.should_quit {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Perform a terminal-affecting action (currently: the nvim handoff).
|
|
fn perform<B: heph_tui::Backend>(
|
|
terminal: &mut ratatui::DefaultTerminal,
|
|
app: &mut App<B>,
|
|
socket: &std::path::Path,
|
|
action: Action,
|
|
) -> Result<()> {
|
|
match action {
|
|
Action::EditContext(id) => {
|
|
if let Err(e) = editor::edit_in_nvim(terminal, &id, socket) {
|
|
app.status = format!("error: {e}");
|
|
}
|
|
app.reload(); // pick up edits made in nvim
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Translate a key press into an [`App`] mutation and/or an [`Action`] for the
|
|
/// event loop to perform.
|
|
fn handle_key<B: heph_tui::Backend>(app: &mut App<B>, key: KeyEvent) -> Option<Action> {
|
|
// Any keypress clears a stale status message.
|
|
app.status.clear();
|
|
|
|
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
|
|
app.should_quit = true;
|
|
return None;
|
|
}
|
|
|
|
match key.code {
|
|
KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
|
|
KeyCode::Char('r') => app.reload(),
|
|
KeyCode::Tab => app.toggle_focus(),
|
|
KeyCode::Char('j') | KeyCode::Down => move_down(app),
|
|
KeyCode::Char('k') | KeyCode::Up => move_up(app),
|
|
KeyCode::Char('h') | KeyCode::Left => app.focus_sidebar(),
|
|
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => app.focus_tasks(),
|
|
// triage mutations (act on the highlighted task)
|
|
KeyCode::Char('x') => app.complete_selected(),
|
|
KeyCode::Char('d') => app.drop_selected(),
|
|
KeyCode::Char('s') => app.skip_selected(),
|
|
KeyCode::Char('A') => app.cycle_attention_selected(),
|
|
KeyCode::Char('b') => app.push_to_blue_selected(),
|
|
// open the task's context doc in nvim (handled by the event loop)
|
|
KeyCode::Char('o') => return app.selected_context_id().map(Action::EditContext),
|
|
_ => {}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn move_down<B: heph_tui::Backend>(app: &mut App<B>) {
|
|
match app.focus {
|
|
Focus::Sidebar => app.move_sidebar(1),
|
|
Focus::Tasks => app.move_task(1),
|
|
}
|
|
}
|
|
|
|
fn move_up<B: heph_tui::Backend>(app: &mut App<B>) {
|
|
match app.focus {
|
|
Focus::Sidebar => app.move_sidebar(-1),
|
|
Focus::Tasks => app.move_task(-1),
|
|
}
|
|
}
|