feat(tui): Sprint 0 complete — panic hook, resize, TuiError, unicode-width
- S0-2: install_panic_hook() restores terminal on crash (disable raw mode, leave alt screen, show cursor) before printing panic message - S0-3: Handle Event::Resize in TUI event loop via mark_resize() - S0-4: Add unicode-width = "0.2" dependency for CJK-aware wrapping - S0-5: Create src/tui_error.rs with TuiError enum (Io, Terminal, Runtime, Config) - Wire panic hook into run_tui_repl with clean restore on exit - Deprecation warnings fixed (PanicHookInfo instead of PanicHookInfo) 108 tests pass. 1 pre-existing failure (sandbox status contract) unrelated. Authored by TheArchitectit
This commit is contained in:
parent
294ab2ab66
commit
7c34488042
|
|
@ -25,6 +25,7 @@ syntect = "5"
|
|||
tokio = { version = "1", features = ["rt-multi-thread", "signal", "time"] }
|
||||
tools = { path = "../tools" }
|
||||
log = "0.4"
|
||||
unicode-width = "0.2"
|
||||
|
||||
|
||||
[lints]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ mod input;
|
|||
mod render;
|
||||
mod setup_wizard;
|
||||
mod tui;
|
||||
mod tui_error;
|
||||
mod tui_update;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
|
@ -7145,6 +7146,7 @@ fn run_repl(
|
|||
/// Run the REPL inside the split-pane TUI. Unlike the plain REPL,
|
||||
/// all output goes into the TUI conversation pane instead of stdout.
|
||||
fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let prev_hook = tui_update::install_panic_hook();
|
||||
let dashboard_state = tui::SharedDashboardState::default();
|
||||
{
|
||||
let mut ds = dashboard_state.write().unwrap();
|
||||
|
|
@ -7259,6 +7261,7 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
tui_update::restore_panic_hook(prev_hook);
|
||||
app.restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,6 +252,13 @@ impl TuiApp {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle terminal resize — force full re-render at new dimensions.
|
||||
pub fn mark_resize(&mut self) {
|
||||
// ratatui Terminal picks up new size on next draw via f.area().
|
||||
// We just need to force a redraw so word-wrapping recalculates.
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Conversation helpers
|
||||
// -------------------------------------------------------------------
|
||||
|
|
@ -340,9 +347,15 @@ impl TuiApp {
|
|||
|
||||
pub fn read_line(&mut self) -> io::Result<TuiReadOutcome> {
|
||||
if event::poll(std::time::Duration::from_millis(16))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
self.needs_redraw = true;
|
||||
return self.handle_key(key);
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
self.needs_redraw = true;
|
||||
return self.handle_key(key);
|
||||
}
|
||||
Event::Resize(_width, _height) => {
|
||||
self.mark_resize();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TuiError {
|
||||
Io(std::io::Error),
|
||||
Terminal(String),
|
||||
Runtime(String),
|
||||
Config(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for TuiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TuiError::Io(e) => write!(f, "TUI I/O error: {e}"),
|
||||
TuiError::Terminal(msg) => write!(f, "Terminal error: {msg}"),
|
||||
TuiError::Runtime(msg) => write!(f, "Runtime error: {msg}"),
|
||||
TuiError::Config(msg) => write!(f, "Config error: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TuiError {}
|
||||
|
||||
impl From<std::io::Error> for TuiError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
TuiError::Io(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,52 @@
|
|||
// LiveCli-dependent code (update_dashboard, run_tui_repl) remains in main.rs
|
||||
// until LiveCli is made pub(crate).
|
||||
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
|
||||
use crate::tui::SharedDashboardState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Panic hook — restores terminal state on crash
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Install a panic hook that restores terminal state before printing the panic.
|
||||
/// Returns the previous hook so it can be restored on clean exit.
|
||||
pub fn install_panic_hook() -> Box<dyn Fn(&panic::PanicHookInfo<'_>) + Send + Sync + 'static> {
|
||||
let prev_hook = panic::take_hook();
|
||||
|
||||
panic::set_hook(Box::new(|info| {
|
||||
// Best-effort terminal cleanup — ignore errors since we're panicking
|
||||
use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen};
|
||||
let _ = disable_raw_mode();
|
||||
let _ = crossterm::execute!(std::io::stdout(), LeaveAlternateScreen);
|
||||
let _ = crossterm::execute!(std::io::stdout(), crossterm::cursor::Show);
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let payload = if let Some(s) = info.payload().downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = info.payload().downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"Box<dyn Any>".to_string()
|
||||
};
|
||||
|
||||
let location = info.location()
|
||||
.map(|l| format!(" at {}:{}", l.file(), l.line()))
|
||||
.unwrap_or_default();
|
||||
|
||||
eprintln!("\n🦀 Claw TUI crashed{location}: {payload}");
|
||||
eprintln!("Terminal has been restored to normal mode.\n");
|
||||
}));
|
||||
|
||||
prev_hook
|
||||
}
|
||||
|
||||
/// Restore the previous panic hook on clean exit.
|
||||
pub fn restore_panic_hook(hook: Box<dyn Fn(&panic::PanicHookInfo<'_>) + Send + Sync + 'static>) {
|
||||
panic::set_hook(hook);
|
||||
}
|
||||
|
||||
/// Strip ANSI escape sequences from text.
|
||||
/// Handles CSI (ESC [), OSC (ESC ]...BEL/ST), and DCS/ESC sequences.
|
||||
pub fn strip_ansi(text: &str) -> String {
|
||||
|
|
|
|||
Loading…
Reference in New Issue