nandi/jolt-nativepublic Fork 0
dce285fb5a5ec1f331b8afa7b2bdc4ed5e1bbd46
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 · 209 lines · 7.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};
14use crossterm::terminal::{
15 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
16};
17use crossterm::{cursor, execute, queue, style};
18
19use crate::keys;
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago20use crate::screen::{self, attr, Color, Screen, Style};
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago21
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago22/// How far one notch of the wheel moves a list, in rows.
23const WHEEL_ROWS: i32 = 3;
24
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago25/// One thing that happened, in the vocabulary [`crate::ui::Ui`] takes.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum Input {
28 Key(String),
29 Click(u16, u16),
30 Wheel(u16, u16, i32),
31 Resize(u16, u16),
32}
33
34pub struct Term {
35 out: Stdout,
36 /// What is on the screen now, so a frame only sends what changed.
37 last: Screen,
38 mouse: bool,
39}
40
41impl Term {
42 /// Take the terminal: raw mode, the alternate screen, mouse reporting, and
43 /// no cursor until a focused entry asks for one.
44 pub fn open(mouse: bool) -> io::Result<Self> {
45 let (w, h) = crossterm::terminal::size().unwrap_or((80, 24));
46 let mut out = io::stdout();
47 enable_raw_mode()?;
48 execute!(out, EnterAlternateScreen, cursor::Hide)?;
49 if mouse {
50 execute!(out, EnableMouseCapture)?;
51 }
52 Ok(Self {
53 out,
54 last: Screen::new(w, h),
55 mouse,
56 })
57 }
58
59 pub fn size(&self) -> (u16, u16) {
60 crossterm::terminal::size().unwrap_or((80, 24))
61 }
62
63 /// Give the terminal back. Called from `tui_close`, and again from a panic
64 /// hook — leaving a shell in raw mode with no cursor is the one failure a
65 /// TUI must not have.
66 pub fn close(&mut self) {
67 if self.mouse {
68 let _ = execute!(self.out, DisableMouseCapture);
69 }
70 let _ = execute!(self.out, cursor::Show, LeaveAlternateScreen);
71 let _ = disable_raw_mode();
72 }
73
74 /// Wait up to `timeout_ms` for input and answer everything that had
75 /// arrived. A zero timeout is a poll.
76 pub fn poll(&mut self, timeout_ms: u64) -> Vec<Input> {
77 let mut out = Vec::new();
78 let deadline = std::time::Duration::from_millis(timeout_ms);
79 if !crossterm::event::poll(deadline).unwrap_or(false) {
80 return out;
81 }
82 // Drain what is queued rather than one event a call: a held arrow key
83 // or a paste arrives as a burst, and handling one per frame would make
84 // the UI lag behind the keyboard.
85 loop {
86 match crossterm::event::read() {
87 Ok(Event::Key(key)) => {
88 // Windows reports both press and release; a release would
89 // type every character twice.
90 if key.kind != KeyEventKind::Release {
91 if let Some(name) = keys::name(key) {
92 out.push(Input::Key(name));
93 }
94 }
95 }
96 Ok(Event::Resize(w, h)) => out.push(Input::Resize(w, h)),
97 Ok(Event::Mouse(mouse)) => match mouse.kind {
98 MouseEventKind::Down(MouseButton::Left) => {
99 out.push(Input::Click(mouse.column, mouse.row))
100 }
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago101 // Three rows a notch. A terminal reports one event per
102 // detent and a row is the whole of a line here, so a row a
103 // notch is a backlog that takes forty of them to cross a
104 // screen — which reads as a list that will not move rather
105 // than one moving slowly.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago106 MouseEventKind::ScrollDown => {
Scroll the list under the pointer, and from where it actually is a785201 nandi 16d ago107 out.push(Input::Wheel(mouse.column, mouse.row, WHEEL_ROWS))
108 }
109 MouseEventKind::ScrollUp => {
110 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 ago111 }
112 _ => {}
113 },
114 Ok(_) => {}
115 Err(_) => break,
116 }
117 if !crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) {
118 break;
119 }
120 }
121 out
122 }
123
124 /// Send whatever differs between `screen` and what is already up there.
125 pub fn flush(&mut self, screen: &Screen, cursor: Option<(u16, u16)>) -> io::Result<()> {
126 if (self.last.width(), self.last.height()) != (screen.width(), screen.height()) {
127 self.last = Screen::new(screen.width(), screen.height());
128 queue!(
129 self.out,
130 crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
131 )?;
132 }
133 let mut style = None;
134 let mut at: Option<(u16, u16)> = None;
135 let width = screen.width();
136 for (i, cell) in screen.cells().iter().enumerate() {
137 let (x, y) = ((i as u16) % width, (i as u16) / width);
138 if self.last.cell(x, y) == Some(cell) {
139 continue;
140 }
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago141 // The right half of a double-width glyph is not written: the
142 // character to its left was drawn across both cells and left the
143 // cursor past them. Writing here would put a second copy of
144 // whatever follows one column over, and every column after it on
145 // that row would be a column out — which is what a mouse click is
146 // then aimed at.
147 if cell.trail {
148 continue;
149 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago150 // Only move when the run breaks: a full-width change is one seek
151 // and a line of text, not a seek a cell.
152 if at != Some((x, y)) {
153 queue!(self.out, cursor::MoveTo(x, y))?;
154 }
155 if style != Some(cell.style) {
156 write_style(&mut self.out, cell.style)?;
157 style = Some(cell.style);
158 }
159 queue!(self.out, style::Print(cell.ch))?;
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago160 at = Some((x + screen::char_cols(cell.ch), y));
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago161 }
162 queue!(self.out, style::ResetColor)?;
163 match cursor {
164 Some((x, y)) => queue!(self.out, cursor::MoveTo(x, y), cursor::Show)?,
165 None => queue!(self.out, cursor::Hide)?,
166 }
167 self.out.flush()?;
168 self.last = screen.clone();
169 Ok(())
170 }
171}
172
173impl Drop for Term {
174 fn drop(&mut self) {
175 self.close();
176 }
177}
178
179fn convert(color: Color) -> style::Color {
180 match color {
181 Color::Default => style::Color::Reset,
182 // The terminal downgrades the 256-colour palette itself, which is the
183 // only place that knows how many colours it really has.
184 Color::Indexed(i) => style::Color::AnsiValue(i),
185 Color::Rgb(r, g, b) => style::Color::Rgb { r, g, b },
186 }
187}
188
189fn write_style(out: &mut Stdout, style: Style) -> io::Result<()> {
190 use style::Attribute;
191 queue!(out, style::SetAttribute(Attribute::Reset))?;
192 for (bit, on) in [
193 (attr::BOLD, Attribute::Bold),
194 (attr::DIM, Attribute::Dim),
195 (attr::UNDERLINE, Attribute::Underlined),
196 (attr::REVERSE, Attribute::Reverse),
197 (attr::BLINK, Attribute::SlowBlink),
198 (attr::ITALIC, Attribute::Italic),
199 ] {
200 if style.has(bit) {
201 queue!(out, style::SetAttribute(on))?;
202 }
203 }
204 queue!(
205 out,
206 style::SetForegroundColor(convert(style.fg)),
207 style::SetBackgroundColor(convert(style.bg))
208 )
209}