1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//! Terminal lifecycle: enter/leave alternate screen, raw mode, mouse, paste.
//!
//! Extracted from `mod.rs` so `run()` doesn't have to spell out the dance.
use std::io;
use crossterm::{
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
type Term = Terminal<CrosstermBackend<io::Stdout>>;
/// Enable raw mode, switch to the alternate screen, enable mouse capture and
/// bracketed paste, then build a ratatui `Terminal`.
pub(super) fn setup_terminal() -> synaps_cli::Result<Term> {
enable_raw_mode().map_err(|e| {
synaps_cli::error::RuntimeError::Tool(format!("terminal setup failed: {}", e))
})?;
let mut stdout = io::stdout();
execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
EnableBracketedPaste
)
.map_err(|e| {
synaps_cli::error::RuntimeError::Tool(format!("terminal setup failed: {}", e))
})?;
// Best-effort: enable the kitty keyboard protocol so modifier-heavy
// chords (Ctrl+Alt+V, Ctrl+Shift+letter, etc.) report correctly on
// terminals that support it (kitty, wezterm, foot, iterm2, alacritty).
// Terminals that don't support it ignore the escape sequence, so we
// swallow any error.
let _ = execute!(
stdout,
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
)
);
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).map_err(|e| {
synaps_cli::error::RuntimeError::Tool(format!("terminal setup failed: {}", e))
})?;
// Synaps renders its own input cursor in the ratatui buffer. Keeping the
// hardware cursor hidden for the whole TUI frame lifecycle prevents it from
// becoming visible at transient backend draw/scrub positions during
// high-frequency streaming redraws.
terminal.hide_cursor().ok();
Ok(terminal)
}
pub(super) fn emergency_teardown_terminal() {
disable_raw_mode().ok();
let mut stdout = io::stdout();
let _ = execute!(stdout, PopKeyboardEnhancementFlags);
execute!(
stdout,
DisableBracketedPaste,
DisableMouseCapture,
LeaveAlternateScreen
)
.ok();
}
/// Reverse of `setup_terminal`: drop raw mode, leave alt screen, restore cursor.
/// Best-effort — errors are swallowed (we are usually exiting anyway).
pub(super) fn teardown_terminal(terminal: &mut Term) {
emergency_teardown_terminal();
// Restore the real terminal cursor on exit. It is intentionally kept hidden
// while the TUI is active because the chat input draws a stable block cursor
// as part of the ratatui frame.
terminal.show_cursor().ok();
}