nandi/jolt-nativepublic Fork 0
65272e3e9431369b8ac5bf985825fb43c8a7048a
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 · 270 lines · 10.6 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;
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago25use crate::screen::{self, 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();
177 for (i, cell) in screen.cells().iter().enumerate() {
178 let (x, y) = ((i as u16) % width, (i as u16) / width);
179 if self.last.cell(x, y) == Some(cell) {
180 continue;
181 }
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago182 // The right half of a double-width glyph is not written: the
183 // character to its left was drawn across both cells and left the
184 // cursor past them. Writing here would put a second copy of
185 // whatever follows one column over, and every column after it on
186 // that row would be a column out — which is what a mouse click is
187 // then aimed at.
188 if cell.trail {
189 continue;
190 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago191 // Only move when the run breaks: a full-width change is one seek
192 // and a line of text, not a seek a cell.
193 if at != Some((x, y)) {
194 queue!(self.out, cursor::MoveTo(x, y))?;
195 }
196 if style != Some(cell.style) {
197 write_style(&mut self.out, cell.style)?;
198 style = Some(cell.style);
199 }
Keep a glyph whole, selector and all, and copy it across whole 3dd441e nandi 16d ago200 // The whole glyph, first character and rest: a terminal handed an
201 // arrow without the selector that follows it draws the small mono
202 // arrow rather than the emoji.
203 let mut glyph = cell.ch.to_string();
204 if let Some(tail) = &cell.tail {
205 glyph.push_str(tail);
206 }
207 queue!(self.out, style::Print(&glyph))?;
208 at = Some((x + screen::glyph_cols(&glyph), y));
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago209 }
210 queue!(self.out, style::ResetColor)?;
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago211 // The pictures after the text, and before the cursor is put back: a
212 // placement is drawn where the cursor is, so it moves the cursor, and
213 // whatever the frame decided about the caret has to be the last word.
214 let cell = graphics::cell();
215 self.graphics.sync(&mut self.out, images, cell)?;
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago216 match cursor {
217 Some((x, y)) => queue!(self.out, cursor::MoveTo(x, y), cursor::Show)?,
218 None => queue!(self.out, cursor::Hide)?,
219 }
220 self.out.flush()?;
221 self.last = screen.clone();
222 Ok(())
223 }
224}
225
226impl Drop for Term {
227 fn drop(&mut self) {
228 self.close();
229 }
230}
231
232fn convert(color: Color) -> style::Color {
233 match color {
234 Color::Default => style::Color::Reset,
235 // The terminal downgrades the 256-colour palette itself, which is the
236 // only place that knows how many colours it really has.
237 Color::Indexed(i) => style::Color::AnsiValue(i),
238 Color::Rgb(r, g, b) => style::Color::Rgb { r, g, b },
239 }
240}
241
242fn write_style(out: &mut Stdout, style: Style) -> io::Result<()> {
243 use style::Attribute;
244 queue!(out, style::SetAttribute(Attribute::Reset))?;
245 for (bit, on) in [
246 (attr::BOLD, Attribute::Bold),
247 (attr::DIM, Attribute::Dim),
248 (attr::UNDERLINE, Attribute::Underlined),
249 (attr::REVERSE, Attribute::Reverse),
250 (attr::BLINK, Attribute::SlowBlink),
251 (attr::ITALIC, Attribute::Italic),
252 ] {
253 if style.has(bit) {
254 queue!(out, style::SetAttribute(on))?;
255 }
256 }
257 queue!(
258 out,
259 style::SetForegroundColor(convert(style.fg)),
260 style::SetBackgroundColor(convert(style.bg))
261 )
262}
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago263
264/// What one cell measures, asked of the terminal.
265fn measure_cell() -> (u16, u16) {
266 match crossterm::terminal::window_size() {
267 Ok(size) => graphics::cell_pixels(size.columns, size.rows, size.width, size.height),
268 Err(_) => graphics::cell_pixels(0, 0, 0, 0),
269 }
270}