hephaestus/crates/heph-tui/src/editor.rs
Erich Blume ae2eff401c feat(tui): heph-tui T2b — nvim context handoff (§8.1)
`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>
2026-06-03 07:11:47 -07:00

53 lines
1.8 KiB
Rust

//! The nvim context handoff (tech-spec §8.1): suspend the TUI, open the task's
//! canonical-context doc in the owner's nvim (live, via heph.nvim's buffer
//! surface), then resume. The KB↔task fusion — edit the description/checklist
//! in the real editor and come straight back to triage.
use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result};
use ratatui::crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::DefaultTerminal;
/// The nvim arguments that open a heph node buffer through heph.nvim. Node ids
/// are ULIDs (no quote/escaping hazard), so a plain `'<id>'` literal is safe.
pub fn nvim_args(node_id: &str) -> Vec<String> {
vec![format!("+lua require('heph.node').open('{node_id}')")]
}
/// Drop out of the alternate screen, run `nvim` on `node_id` (pointing its
/// heph.nvim at the same daemon via `$HEPH_SOCKET`), then restore the TUI.
pub fn edit_in_nvim(terminal: &mut DefaultTerminal, node_id: &str, socket: &Path) -> Result<()> {
disable_raw_mode()?;
execute!(std::io::stdout(), LeaveAlternateScreen)?;
let status = Command::new("nvim")
.args(nvim_args(node_id))
.env("HEPH_SOCKET", socket)
.status();
// Restore the TUI regardless of how nvim exited, then surface any error.
enable_raw_mode()?;
execute!(std::io::stdout(), EnterAlternateScreen)?;
terminal.clear()?;
status.context("launching nvim (is it on PATH?)")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn args_open_the_node_via_heph_nvim() {
let args = nvim_args("01ABCXYZ");
assert_eq!(args.len(), 1);
assert!(args[0].starts_with("+lua"));
assert!(args[0].contains("require('heph.node').open('01ABCXYZ')"));
}
}