use std::io::{self, Stdout, Write};
use std::path::Path;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use miette::{IntoDiagnostic, Result, WrapErr, miette};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::{TerminalOptions, Viewport};
use crate::commands::init::commit::{CommitPlan, plan_commit};
use crate::commands::init::tui::dispatch::{
apply_cycle_outcome_signal, apply_expand_outcome_signal, dispatch,
};
use crate::commands::init::tui::render::{render_confirmation_screen, render_tree_screen};
use crate::commands::init::tui::tree_io::{load_children_for_focused, reload_tree_for_visibility};
use crate::commands::init::tui::{DispatchResult, ScaffoldOutcome, TuiState};
pub fn run_interactive_scaffold(cwd_host: &Path) -> Result<ScaffoldOutcome> {
let mut state = TuiState::new(cwd_host.to_path_buf())?;
let mut terminal = enter_terminal()?;
let mut guard = TerminalGuard::armed();
let outcome = drive_tree_screen(&mut terminal, &mut state);
let outcome = match outcome {
Ok(DriverExit::Commit) => {
let plan = plan_commit(&state.tree, cwd_host)
.map_err(|err| miette!("{err}"))
.wrap_err("haz init: AUX-005 commit-plan construction failed")?;
drive_confirmation_screen(&mut terminal, &plan)?
}
Ok(DriverExit::Cancel) => ScaffoldOutcome::Declined,
Ok(DriverExit::Abort) => ScaffoldOutcome::Aborted,
Err(e) => {
guard.disarm();
restore_terminal();
return Err(e);
}
};
guard.disarm();
restore_terminal();
Ok(outcome)
}
#[derive(Debug)]
enum DriverExit {
Commit,
Cancel,
Abort,
}
fn drive_tree_screen(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
state: &mut TuiState,
) -> Result<DriverExit> {
loop {
terminal
.draw(|frame| render_tree_screen(frame, state))
.into_diagnostic()
.wrap_err("haz init: failed to draw tree screen")?;
if state.bell_pending {
ring_bell();
state.bell_pending = false;
}
let event = read_event_blocking()?;
match dispatch(&event, state) {
DispatchResult::Continue => {}
DispatchResult::LoadChildrenThenRetryExpand => {
load_children_for_focused(state)?;
let outcome = state.tree.try_expand_focused();
apply_expand_outcome_signal(state, outcome);
}
DispatchResult::LoadChildrenThenRetryCycle => {
load_children_for_focused(state)?;
let outcome = state.tree.cycle_focused();
apply_cycle_outcome_signal(state, outcome);
}
DispatchResult::ReloadTreeForVisibility => {
reload_tree_for_visibility(state)?;
}
DispatchResult::ExitCommit => return Ok(DriverExit::Commit),
DispatchResult::ExitCancel => return Ok(DriverExit::Cancel),
DispatchResult::Abort => return Ok(DriverExit::Abort),
}
}
}
fn drive_confirmation_screen(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
plan: &CommitPlan,
) -> Result<ScaffoldOutcome> {
loop {
terminal
.draw(|frame| render_confirmation_screen(frame, plan))
.into_diagnostic()
.wrap_err("haz init: failed to draw confirmation screen")?;
let event = read_event_blocking()?;
let Event::Key(key) = event else {
continue;
};
if key.kind != KeyEventKind::Press {
continue;
}
if key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char('c' | 'C'))
{
return Ok(ScaffoldOutcome::Aborted);
}
match key.code {
KeyCode::Char('y' | 'Y') => {
return Ok(ScaffoldOutcome::Confirmed(plan.clone()));
}
KeyCode::Char('n' | 'N' | 'q' | 'Q') | KeyCode::Esc => {
return Ok(ScaffoldOutcome::Declined);
}
_ => {}
}
}
}
fn read_event_blocking() -> Result<Event> {
event::read()
.into_diagnostic()
.wrap_err("haz init: failed to read terminal event")
}
fn ring_bell() {
let _ = io::stderr().write_all(b"\x07");
let _ = io::stderr().flush();
}
fn enter_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()
.into_diagnostic()
.wrap_err("haz init: failed to enable terminal raw mode")?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)
.into_diagnostic()
.wrap_err("haz init: failed to enter the alternate screen")?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
.into_diagnostic()
.wrap_err("haz init: failed to initialise the ratatui terminal")?;
Ok(terminal)
}
fn restore_terminal() {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
}
struct TerminalGuard {
armed: bool,
}
impl TerminalGuard {
fn armed() -> Self {
Self { armed: true }
}
fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
if self.armed {
restore_terminal();
}
}
}