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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Crossterm backend that presents each frame atomically.
//!
//! Ratatui paints a frame as cell writes, then shows and moves the hardware
//! cursor, each step flushing separately. Terminals may render between those
//! writes, so the caret visibly jumps across painted cells before settling.
//! Wrapping the frame in synchronized output (DEC private mode 2026) makes
//! supporting terminals hold presentation until the frame is complete; other
//! terminals ignore the sequence.
use std::io::{self, Stdout};
use crossterm::{
execute, queue,
terminal::{
self, enable_raw_mode, BeginSynchronizedUpdate, EndSynchronizedUpdate, EnterAlternateScreen,
},
};
use ratatui::{
backend::{Backend, ClearType, CrosstermBackend, WindowSize},
buffer::Cell,
layout::{Position, Rect, Size},
Terminal,
};
/// Rho's interactive terminal. Every TUI surface draws through this type so no
/// frame bypasses synchronized output; `scripts/architecture.json` forbids
/// ratatui's unsynchronized `init`, `try_init`, and `DefaultTerminal`.
pub(crate) type DefaultTerminal = Terminal<SyncedBackend>;
/// `ratatui::try_init` for [`SyncedBackend`]: install a panic hook that
/// restores the terminal, enter raw mode and the alternate screen.
pub(crate) fn try_init() -> io::Result<DefaultTerminal> {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
ratatui::restore();
hook(info);
}));
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen)?;
Terminal::new(SyncedBackend(CrosstermBackend::new(io::stdout())))
}
/// Panicking [`try_init`], mirroring `ratatui::init`. The panic runs the hook,
/// so a half-initialized terminal is still restored.
pub(crate) fn init() -> DefaultTerminal {
try_init().expect("failed to initialize terminal")
}
/// Forces the next draw to repaint every cell after a terminal resize event.
///
/// Ratatui only clears when the size differs from its last frame. A shrink
/// then restore can coalesce into events that all report the old size, yet
/// the terminal truncated cells on the shrink; a diff-only redraw then leaves
/// them blank. Call this from each surface's `Event::Resize` handler.
pub(crate) fn repaint_after_resize(terminal: &mut DefaultTerminal) -> io::Result<()> {
let size = terminal.size()?;
terminal.resize(Rect::new(0, 0, size.width, size.height))
}
/// Ratatui's crossterm backend with each frame sent as one synchronized
/// update. `Terminal::draw` calls `draw` once, positions the cursor, then calls
/// `flush`, so begin-in-`draw` and end-in-`flush` cover cells and caret
/// together. A clear also begins the update and stays queued, so every clear
/// must be followed by a draw; ratatui's resize and autoresize always are.
/// Both markers are idempotent, so no open-update state is tracked.
pub(crate) struct SyncedBackend(CrosstermBackend<Stdout>);
impl Backend for SyncedBackend {
type Error = io::Error;
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
queue!(self.0, BeginSynchronizedUpdate)?;
self.0.draw(content)
}
fn flush(&mut self) -> io::Result<()> {
queue!(self.0, EndSynchronizedUpdate)?;
Backend::flush(&mut self.0)
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
self.0.append_lines(n)
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.0.hide_cursor()
}
fn show_cursor(&mut self) -> io::Result<()> {
self.0.show_cursor()
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
self.0.get_cursor_position()
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.0.set_cursor_position(position)
}
fn clear(&mut self) -> io::Result<()> {
self.clear_region(ClearType::All)
}
/// Queues the clear inside the next frame's synchronized update instead of
/// flushing it. Ratatui clears before redrawing a resized frame, and a
/// flushed clear shows a blank screen until that frame arrives.
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
let clear_type = match clear_type {
ClearType::All => terminal::ClearType::All,
ClearType::AfterCursor => terminal::ClearType::FromCursorDown,
ClearType::BeforeCursor => terminal::ClearType::FromCursorUp,
ClearType::CurrentLine => terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => terminal::ClearType::UntilNewLine,
};
queue!(self.0, BeginSynchronizedUpdate, terminal::Clear(clear_type))
}
fn size(&self) -> io::Result<Size> {
self.0.size()
}
fn window_size(&mut self) -> io::Result<WindowSize> {
self.0.window_size()
}
}