nandi/jolt-nativepublic Fork 0
789bb134c91bfdee346df2dc9656bf2e7f9bbd1a
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 · 247 lines · 9.5 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};
14use crossterm::terminal::{
15 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
16};
17use crossterm::{cursor, execute, queue, style};
18
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago19use crate::graphics::{self, Graphics, Placement};
Run the formatter over the tree 3e8c6f0 nandi 13d ago20use crate::keys;
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago21use crate::screen::{self, attr, Color, Screen, Style};
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago22
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago23/// How far one notch of the wheel moves a list, in rows.
24const WHEEL_ROWS: i32 = 3;
25
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago26/// One thing that happened, in the vocabulary [`crate::ui::Ui`] takes.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum Input {
29 Key(String),
30 Click(u16, u16),
31 Wheel(u16, u16, i32),
32 Resize(u16, u16),
33}
34
35pub struct Term {
36 out: Stdout,
37 /// What is on the screen now, so a frame only sends what changed.
38 last: Screen,
39 mouse: bool,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago40 /// The pictures the terminal has been given, and where they are.
41 graphics: Graphics,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago42}
43
44impl Term {
45 /// Take the terminal: raw mode, the alternate screen, mouse reporting, and
46 /// no cursor until a focused entry asks for one.
47 pub fn open(mouse: bool) -> io::Result<Self> {
48 let (w, h) = crossterm::terminal::size().unwrap_or((80, 24));
49 let mut out = io::stdout();
50 enable_raw_mode()?;
51 execute!(out, EnterAlternateScreen, cursor::Hide)?;
52 if mouse {
53 execute!(out, EnableMouseCapture)?;
54 }
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago55 graphics::set_cell(measure_cell());
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago56 Ok(Self {
57 out,
58 last: Screen::new(w, h),
59 mouse,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago60 graphics: Graphics::default(),
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago61 })
62 }
63
64 pub fn size(&self) -> (u16, u16) {
65 crossterm::terminal::size().unwrap_or((80, 24))
66 }
67
68 /// Give the terminal back. Called from `tui_close`, and again from a panic
69 /// hook — leaving a shell in raw mode with no cursor is the one failure a
70 /// TUI must not have.
71 pub fn close(&mut self) {
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago72 // The pictures first: a placement is the terminal's, not the screen
73 // buffer's, and one left behind outlives the alternate screen it was
74 // made on.
75 let _ = self.graphics.clear(&mut self.out);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago76 if self.mouse {
77 let _ = execute!(self.out, DisableMouseCapture);
78 }
79 let _ = execute!(self.out, cursor::Show, LeaveAlternateScreen);
80 let _ = disable_raw_mode();
81 }
82
83 /// Wait up to `timeout_ms` for input and answer everything that had
84 /// arrived. A zero timeout is a poll.
85 pub fn poll(&mut self, timeout_ms: u64) -> Vec<Input> {
86 let mut out = Vec::new();
87 let deadline = std::time::Duration::from_millis(timeout_ms);
88 if !crossterm::event::poll(deadline).unwrap_or(false) {
89 return out;
90 }
91 // Drain what is queued rather than one event a call: a held arrow key
92 // or a paste arrives as a burst, and handling one per frame would make
93 // the UI lag behind the keyboard.
94 loop {
95 match crossterm::event::read() {
96 Ok(Event::Key(key)) => {
97 // Windows reports both press and release; a release would
98 // type every character twice.
99 if key.kind != KeyEventKind::Release {
100 if let Some(name) = keys::name(key) {
101 out.push(Input::Key(name));
102 }
103 }
104 }
105 Ok(Event::Resize(w, h)) => out.push(Input::Resize(w, h)),
106 Ok(Event::Mouse(mouse)) => match mouse.kind {
107 MouseEventKind::Down(MouseButton::Left) => {
108 out.push(Input::Click(mouse.column, mouse.row))
109 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago110 // Three rows a notch. A terminal reports one event per
111 // detent and a row is the whole of a line here, so a row a
112 // notch is a backlog that takes forty of them to cross a
113 // screen — which reads as a list that will not move rather
114 // than one moving slowly.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago115 MouseEventKind::ScrollDown => {
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago116 out.push(Input::Wheel(mouse.column, mouse.row, WHEEL_ROWS))
117 }
118 MouseEventKind::ScrollUp => {
119 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 ago120 }
121 _ => {}
122 },
123 Ok(_) => {}
124 Err(_) => break,
125 }
126 if !crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) {
127 break;
128 }
129 }
130 out
131 }
132
133 /// 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 ago134 pub fn flush(
135 &mut self,
136 screen: &Screen,
137 cursor: Option<(u16, u16)>,
138 images: &[Placement],
139 ) -> io::Result<()> {
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago140 if (self.last.width(), self.last.height()) != (screen.width(), screen.height()) {
141 self.last = Screen::new(screen.width(), screen.height());
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago142 // A resize is a new cell size as often as not — a font change, a
143 // window dragged to another screen — and every picture's size is
144 // measured in cells.
145 graphics::set_cell(measure_cell());
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago146 queue!(
147 self.out,
148 crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
149 )?;
150 }
151 let mut style = None;
152 let mut at: Option<(u16, u16)> = None;
153 let width = screen.width();
154 for (i, cell) in screen.cells().iter().enumerate() {
155 let (x, y) = ((i as u16) % width, (i as u16) / width);
156 if self.last.cell(x, y) == Some(cell) {
157 continue;
158 }
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago159 // The right half of a double-width glyph is not written: the
160 // character to its left was drawn across both cells and left the
161 // cursor past them. Writing here would put a second copy of
162 // whatever follows one column over, and every column after it on
163 // that row would be a column out — which is what a mouse click is
164 // then aimed at.
165 if cell.trail {
166 continue;
167 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago168 // Only move when the run breaks: a full-width change is one seek
169 // and a line of text, not a seek a cell.
170 if at != Some((x, y)) {
171 queue!(self.out, cursor::MoveTo(x, y))?;
172 }
173 if style != Some(cell.style) {
174 write_style(&mut self.out, cell.style)?;
175 style = Some(cell.style);
176 }
Keep a glyph whole, selector and all, and copy it across whole 3dd441e nandi 16d ago177 // The whole glyph, first character and rest: a terminal handed an
178 // arrow without the selector that follows it draws the small mono
179 // arrow rather than the emoji.
180 let mut glyph = cell.ch.to_string();
181 if let Some(tail) = &cell.tail {
182 glyph.push_str(tail);
183 }
184 queue!(self.out, style::Print(&glyph))?;
185 at = Some((x + screen::glyph_cols(&glyph), y));
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago186 }
187 queue!(self.out, style::ResetColor)?;
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago188 // The pictures after the text, and before the cursor is put back: a
189 // placement is drawn where the cursor is, so it moves the cursor, and
190 // whatever the frame decided about the caret has to be the last word.
191 let cell = graphics::cell();
192 self.graphics.sync(&mut self.out, images, cell)?;
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago193 match cursor {
194 Some((x, y)) => queue!(self.out, cursor::MoveTo(x, y), cursor::Show)?,
195 None => queue!(self.out, cursor::Hide)?,
196 }
197 self.out.flush()?;
198 self.last = screen.clone();
199 Ok(())
200 }
201}
202
203impl Drop for Term {
204 fn drop(&mut self) {
205 self.close();
206 }
207}
208
209fn convert(color: Color) -> style::Color {
210 match color {
211 Color::Default => style::Color::Reset,
212 // The terminal downgrades the 256-colour palette itself, which is the
213 // only place that knows how many colours it really has.
214 Color::Indexed(i) => style::Color::AnsiValue(i),
215 Color::Rgb(r, g, b) => style::Color::Rgb { r, g, b },
216 }
217}
218
219fn write_style(out: &mut Stdout, style: Style) -> io::Result<()> {
220 use style::Attribute;
221 queue!(out, style::SetAttribute(Attribute::Reset))?;
222 for (bit, on) in [
223 (attr::BOLD, Attribute::Bold),
224 (attr::DIM, Attribute::Dim),
225 (attr::UNDERLINE, Attribute::Underlined),
226 (attr::REVERSE, Attribute::Reverse),
227 (attr::BLINK, Attribute::SlowBlink),
228 (attr::ITALIC, Attribute::Italic),
229 ] {
230 if style.has(bit) {
231 queue!(out, style::SetAttribute(on))?;
232 }
233 }
234 queue!(
235 out,
236 style::SetForegroundColor(convert(style.fg)),
237 style::SetBackgroundColor(convert(style.bg))
238 )
239}
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago240
241/// What one cell measures, asked of the terminal.
242fn measure_cell() -> (u16, u16) {
243 match crossterm::terminal::window_size() {
244 Ok(size) => graphics::cell_pixels(size.columns, size.rows, size.width, size.height),
245 Err(_) => graphics::cell_pixels(0, 0, 0, 0),
246 }
247}