nandi/jolt-nativepublic Fork 0
4706c920e45ce80b11ee106d05c16d9eacc99fc7
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

term.rs · 293 lines · 11.8 KBRust Blame HistoryRaw
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago1//! The terminal itself: raw mode, the alternate screen, and the diff that
2//! turns a grid of cells into the fewest escape sequences that will do.
3//!
4//! Everything above this file paints into a [`Screen`] and never writes a byte,
5//! which is what makes the widget layer testable. This is the one place that
6//! knows a TTY exists, and it is behind the `terminal` feature so a build for a
7//! machine with no terminal crate at all still has the tree and the layout.
8
9use std::io::{self, Stdout, Write};
10
11use crossterm::event::{
12 DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind, MouseButton, MouseEventKind,
13};
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago14use crossterm::event::{
15 KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
16};
17use crossterm::terminal::supports_keyboard_enhancement;
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago18use crossterm::terminal::{
19 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
20};
21use crossterm::{cursor, execute, queue, style};
22
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago23use crate::graphics::{self, Graphics, Placement};
Run the formatter over the tree 3e8c6f0 nandi 13d ago24use crate::keys;
Take a wide glyph off the grid whole, and stop trusting the cursor after one 48212a7 nandi 9d ago25use crate::screen::{attr, Color, Screen, Style};
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago26
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago27/// How far one notch of the wheel moves a list, in rows.
28const WHEEL_ROWS: i32 = 3;
29
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago30/// One thing that happened, in the vocabulary [`crate::ui::Ui`] takes.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub enum Input {
33 Key(String),
34 Click(u16, u16),
35 Wheel(u16, u16, i32),
36 Resize(u16, u16),
37}
38
39pub struct Term {
40 out: Stdout,
41 /// What is on the screen now, so a frame only sends what changed.
42 last: Screen,
43 mouse: bool,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago44 /// The pictures the terminal has been given, and where they are.
45 graphics: Graphics,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago46 /// Whether the keyboard protocol was pushed, and so has to be popped.
47 enhanced: bool,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago48}
49
50impl Term {
51 /// Take the terminal: raw mode, the alternate screen, mouse reporting, and
52 /// no cursor until a focused entry asks for one.
53 pub fn open(mouse: bool) -> io::Result<Self> {
54 let (w, h) = crossterm::terminal::size().unwrap_or((80, 24));
55 let mut out = io::stdout();
56 enable_raw_mode()?;
57 execute!(out, EnterAlternateScreen, cursor::Hide)?;
58 if mouse {
59 execute!(out, EnableMouseCapture)?;
60 }
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago61 // Shift+Enter is a newline in the compose bar and Enter sends the
62 // line, which a terminal can only tell apart when it is asked to: the
63 // legacy encoding gives both of them the same byte. This is the kitty
64 // keyboard protocol's first flag and nothing more — the terminals that
65 // have it answer the query, and the ones that do not are left as they
66 // were, with Alt+Enter and Ctrl+J as the way to break a line there.
67 let enhanced = matches!(supports_keyboard_enhancement(), Ok(true));
68 if enhanced {
69 let _ = execute!(
70 out,
71 PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
72 );
73 }
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago74 graphics::set_cell(measure_cell());
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago75 Ok(Self {
76 out,
77 last: Screen::new(w, h),
78 mouse,
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago79 enhanced,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago80 graphics: Graphics::default(),
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago81 })
82 }
83
84 pub fn size(&self) -> (u16, u16) {
85 crossterm::terminal::size().unwrap_or((80, 24))
86 }
87
88 /// Give the terminal back. Called from `tui_close`, and again from a panic
89 /// hook — leaving a shell in raw mode with no cursor is the one failure a
90 /// TUI must not have.
91 pub fn close(&mut self) {
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago92 // The pictures first: a placement is the terminal's, not the screen
93 // buffer's, and one left behind outlives the alternate screen it was
94 // made on.
95 let _ = self.graphics.clear(&mut self.out);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago96 if self.mouse {
97 let _ = execute!(self.out, DisableMouseCapture);
98 }
Give the terminal's entry a caret that means what it says 361b4dc nandi 9d ago99 if self.enhanced {
100 let _ = execute!(self.out, PopKeyboardEnhancementFlags);
101 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago102 let _ = execute!(self.out, cursor::Show, LeaveAlternateScreen);
103 let _ = disable_raw_mode();
104 }
105
106 /// Wait up to `timeout_ms` for input and answer everything that had
107 /// arrived. A zero timeout is a poll.
108 pub fn poll(&mut self, timeout_ms: u64) -> Vec<Input> {
109 let mut out = Vec::new();
110 let deadline = std::time::Duration::from_millis(timeout_ms);
111 if !crossterm::event::poll(deadline).unwrap_or(false) {
112 return out;
113 }
114 // Drain what is queued rather than one event a call: a held arrow key
115 // or a paste arrives as a burst, and handling one per frame would make
116 // the UI lag behind the keyboard.
117 loop {
118 match crossterm::event::read() {
119 Ok(Event::Key(key)) => {
120 // Windows reports both press and release; a release would
121 // type every character twice.
122 if key.kind != KeyEventKind::Release {
123 if let Some(name) = keys::name(key) {
124 out.push(Input::Key(name));
125 }
126 }
127 }
128 Ok(Event::Resize(w, h)) => out.push(Input::Resize(w, h)),
129 Ok(Event::Mouse(mouse)) => match mouse.kind {
130 MouseEventKind::Down(MouseButton::Left) => {
131 out.push(Input::Click(mouse.column, mouse.row))
132 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago133 // Three rows a notch. A terminal reports one event per
134 // detent and a row is the whole of a line here, so a row a
135 // notch is a backlog that takes forty of them to cross a
136 // screen — which reads as a list that will not move rather
137 // than one moving slowly.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago138 MouseEventKind::ScrollDown => {
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago139 out.push(Input::Wheel(mouse.column, mouse.row, WHEEL_ROWS))
140 }
141 MouseEventKind::ScrollUp => {
142 out.push(Input::Wheel(mouse.column, mouse.row, -WHEEL_ROWS))
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago143 }
144 _ => {}
145 },
146 Ok(_) => {}
147 Err(_) => break,
148 }
149 if !crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) {
150 break;
151 }
152 }
153 out
154 }
155
156 /// Send whatever differs between `screen` and what is already up there.
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago157 pub fn flush(
158 &mut self,
159 screen: &Screen,
160 cursor: Option<(u16, u16)>,
161 images: &[Placement],
162 ) -> io::Result<()> {
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago163 if (self.last.width(), self.last.height()) != (screen.width(), screen.height()) {
164 self.last = Screen::new(screen.width(), screen.height());
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago165 // A resize is a new cell size as often as not — a font change, a
166 // window dragged to another screen — and every picture's size is
167 // measured in cells.
168 graphics::set_cell(measure_cell());
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago169 queue!(
170 self.out,
171 crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
172 )?;
173 }
174 let mut style = None;
175 let mut at: Option<(u16, u16)> = None;
176 let width = screen.width();
Take a wide glyph off the grid whole, and stop trusting the cursor after one 48212a7 nandi 9d ago177 let cells = screen.cells();
178 for (i, cell) in cells.iter().enumerate() {
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago179 let (x, y) = ((i as u16) % width, (i as u16) / width);
Take a wide glyph off the grid whole, and stop trusting the cursor after one 48212a7 nandi 9d ago180 let wide = !cell.trail && x + 1 < width && cells.get(i + 1).is_some_and(|c| c.trail);
181 // A wide glyph is its two cells together: when only the right
182 // half changed, a terminal has lost the whole glyph, so the left
183 // half is written again as well.
184 let changed = self.last.cell(x, y) != Some(cell)
185 || (wide && self.last.cell(x + 1, y) != cells.get(i + 1));
186 if !changed {
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago187 continue;
188 }
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago189 // The right half of a double-width glyph is not written: the
190 // character to its left was drawn across both cells and left the
191 // cursor past them. Writing here would put a second copy of
192 // whatever follows one column over, and every column after it on
193 // that row would be a column out — which is what a mouse click is
194 // then aimed at.
195 if cell.trail {
196 continue;
197 }
Take a wide glyph off the grid whole, and stop trusting the cursor after one 48212a7 nandi 9d ago198 if wide {
199 // Blank the right half first. Terminals do not agree on how
200 // wide `✏️` or `↩️` is — a symbol carrying the emoji selector
201 // is two columns in some and one in others — and one that
202 // draws it narrow would otherwise leave whatever stood in that
203 // column standing beside it.
204 if style != Some(cell.style) {
205 write_style(&mut self.out, cell.style)?;
206 style = Some(cell.style);
207 }
208 queue!(self.out, cursor::MoveTo(x + 1, y), style::Print(' '))?;
209 at = None;
210 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago211 // Only move when the run breaks: a full-width change is one seek
212 // and a line of text, not a seek a cell.
213 if at != Some((x, y)) {
214 queue!(self.out, cursor::MoveTo(x, y))?;
215 }
216 if style != Some(cell.style) {
217 write_style(&mut self.out, cell.style)?;
218 style = Some(cell.style);
219 }
Keep a glyph whole, selector and all, and copy it across whole 3dd441e nandi 16d ago220 // The whole glyph, first character and rest: a terminal handed an
221 // arrow without the selector that follows it draws the small mono
222 // arrow rather than the emoji.
223 let mut glyph = cell.ch.to_string();
224 if let Some(tail) = &cell.tail {
225 glyph.push_str(tail);
226 }
227 queue!(self.out, style::Print(&glyph))?;
Take a wide glyph off the grid whole, and stop trusting the cursor after one 48212a7 nandi 9d ago228 // After a wide glyph, where the cursor went is the terminal's
229 // opinion rather than ours, so the next cell seeks instead of
230 // trusting it.
231 at = if wide { None } else { Some((x + 1, y)) };
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago232 }
233 queue!(self.out, style::ResetColor)?;
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago234 // The pictures after the text, and before the cursor is put back: a
235 // placement is drawn where the cursor is, so it moves the cursor, and
236 // whatever the frame decided about the caret has to be the last word.
237 let cell = graphics::cell();
238 self.graphics.sync(&mut self.out, images, cell)?;
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago239 match cursor {
240 Some((x, y)) => queue!(self.out, cursor::MoveTo(x, y), cursor::Show)?,
241 None => queue!(self.out, cursor::Hide)?,
242 }
243 self.out.flush()?;
244 self.last = screen.clone();
245 Ok(())
246 }
247}
248
249impl Drop for Term {
250 fn drop(&mut self) {
251 self.close();
252 }
253}
254
255fn convert(color: Color) -> style::Color {
256 match color {
257 Color::Default => style::Color::Reset,
258 // The terminal downgrades the 256-colour palette itself, which is the
259 // only place that knows how many colours it really has.
260 Color::Indexed(i) => style::Color::AnsiValue(i),
261 Color::Rgb(r, g, b) => style::Color::Rgb { r, g, b },
262 }
263}
264
265fn write_style(out: &mut Stdout, style: Style) -> io::Result<()> {
266 use style::Attribute;
267 queue!(out, style::SetAttribute(Attribute::Reset))?;
268 for (bit, on) in [
269 (attr::BOLD, Attribute::Bold),
270 (attr::DIM, Attribute::Dim),
271 (attr::UNDERLINE, Attribute::Underlined),
272 (attr::REVERSE, Attribute::Reverse),
273 (attr::BLINK, Attribute::SlowBlink),
274 (attr::ITALIC, Attribute::Italic),
275 ] {
276 if style.has(bit) {
277 queue!(out, style::SetAttribute(on))?;
278 }
279 }
280 queue!(
281 out,
282 style::SetForegroundColor(convert(style.fg)),
283 style::SetBackgroundColor(convert(style.bg))
284 )
285}
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago286
287/// What one cell measures, asked of the terminal.
288fn measure_cell() -> (u16, u16) {
289 match crossterm::terminal::window_size() {
290 Ok(size) => graphics::cell_pixels(size.columns, size.rows, size.width, size.height),
291 Err(_) => graphics::cell_pixels(0, 0, 0, 0),
292 }
293}