nandi/jolt-nativepublic Fork 0
a7f62025fc9a5a5db4edb9a6ba6808dc31f7596b
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 · 190 lines · 6.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;
20use crate::screen::{attr, Color, Screen, Style};
21
22/// One thing that happened, in the vocabulary [`crate::ui::Ui`] takes.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum Input {
25 Key(String),
26 Click(u16, u16),
27 Wheel(u16, u16, i32),
28 Resize(u16, u16),
29}
30
31pub struct Term {
32 out: Stdout,
33 /// What is on the screen now, so a frame only sends what changed.
34 last: Screen,
35 mouse: bool,
36}
37
38impl Term {
39 /// Take the terminal: raw mode, the alternate screen, mouse reporting, and
40 /// no cursor until a focused entry asks for one.
41 pub fn open(mouse: bool) -> io::Result<Self> {
42 let (w, h) = crossterm::terminal::size().unwrap_or((80, 24));
43 let mut out = io::stdout();
44 enable_raw_mode()?;
45 execute!(out, EnterAlternateScreen, cursor::Hide)?;
46 if mouse {
47 execute!(out, EnableMouseCapture)?;
48 }
49 Ok(Self {
50 out,
51 last: Screen::new(w, h),
52 mouse,
53 })
54 }
55
56 pub fn size(&self) -> (u16, u16) {
57 crossterm::terminal::size().unwrap_or((80, 24))
58 }
59
60 /// Give the terminal back. Called from `tui_close`, and again from a panic
61 /// hook — leaving a shell in raw mode with no cursor is the one failure a
62 /// TUI must not have.
63 pub fn close(&mut self) {
64 if self.mouse {
65 let _ = execute!(self.out, DisableMouseCapture);
66 }
67 let _ = execute!(self.out, cursor::Show, LeaveAlternateScreen);
68 let _ = disable_raw_mode();
69 }
70
71 /// Wait up to `timeout_ms` for input and answer everything that had
72 /// arrived. A zero timeout is a poll.
73 pub fn poll(&mut self, timeout_ms: u64) -> Vec<Input> {
74 let mut out = Vec::new();
75 let deadline = std::time::Duration::from_millis(timeout_ms);
76 if !crossterm::event::poll(deadline).unwrap_or(false) {
77 return out;
78 }
79 // Drain what is queued rather than one event a call: a held arrow key
80 // or a paste arrives as a burst, and handling one per frame would make
81 // the UI lag behind the keyboard.
82 loop {
83 match crossterm::event::read() {
84 Ok(Event::Key(key)) => {
85 // Windows reports both press and release; a release would
86 // type every character twice.
87 if key.kind != KeyEventKind::Release {
88 if let Some(name) = keys::name(key) {
89 out.push(Input::Key(name));
90 }
91 }
92 }
93 Ok(Event::Resize(w, h)) => out.push(Input::Resize(w, h)),
94 Ok(Event::Mouse(mouse)) => match mouse.kind {
95 MouseEventKind::Down(MouseButton::Left) => {
96 out.push(Input::Click(mouse.column, mouse.row))
97 }
98 MouseEventKind::ScrollDown => {
99 out.push(Input::Wheel(mouse.column, mouse.row, 1))
100 }
101 MouseEventKind::ScrollUp => out.push(Input::Wheel(mouse.column, mouse.row, -1)),
102 _ => {}
103 },
104 Ok(_) => {}
105 Err(_) => break,
106 }
107 if !crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) {
108 break;
109 }
110 }
111 out
112 }
113
114 /// Send whatever differs between `screen` and what is already up there.
115 pub fn flush(&mut self, screen: &Screen, cursor: Option<(u16, u16)>) -> io::Result<()> {
116 if (self.last.width(), self.last.height()) != (screen.width(), screen.height()) {
117 self.last = Screen::new(screen.width(), screen.height());
118 queue!(
119 self.out,
120 crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
121 )?;
122 }
123 let mut style = None;
124 let mut at: Option<(u16, u16)> = None;
125 let width = screen.width();
126 for (i, cell) in screen.cells().iter().enumerate() {
127 let (x, y) = ((i as u16) % width, (i as u16) / width);
128 if self.last.cell(x, y) == Some(cell) {
129 continue;
130 }
131 // Only move when the run breaks: a full-width change is one seek
132 // and a line of text, not a seek a cell.
133 if at != Some((x, y)) {
134 queue!(self.out, cursor::MoveTo(x, y))?;
135 }
136 if style != Some(cell.style) {
137 write_style(&mut self.out, cell.style)?;
138 style = Some(cell.style);
139 }
140 queue!(self.out, style::Print(cell.ch))?;
141 at = Some((x + 1, y));
142 }
143 queue!(self.out, style::ResetColor)?;
144 match cursor {
145 Some((x, y)) => queue!(self.out, cursor::MoveTo(x, y), cursor::Show)?,
146 None => queue!(self.out, cursor::Hide)?,
147 }
148 self.out.flush()?;
149 self.last = screen.clone();
150 Ok(())
151 }
152}
153
154impl Drop for Term {
155 fn drop(&mut self) {
156 self.close();
157 }
158}
159
160fn convert(color: Color) -> style::Color {
161 match color {
162 Color::Default => style::Color::Reset,
163 // The terminal downgrades the 256-colour palette itself, which is the
164 // only place that knows how many colours it really has.
165 Color::Indexed(i) => style::Color::AnsiValue(i),
166 Color::Rgb(r, g, b) => style::Color::Rgb { r, g, b },
167 }
168}
169
170fn write_style(out: &mut Stdout, style: Style) -> io::Result<()> {
171 use style::Attribute;
172 queue!(out, style::SetAttribute(Attribute::Reset))?;
173 for (bit, on) in [
174 (attr::BOLD, Attribute::Bold),
175 (attr::DIM, Attribute::Dim),
176 (attr::UNDERLINE, Attribute::Underlined),
177 (attr::REVERSE, Attribute::Reverse),
178 (attr::BLINK, Attribute::SlowBlink),
179 (attr::ITALIC, Attribute::Italic),
180 ] {
181 if style.has(bit) {
182 queue!(out, style::SetAttribute(on))?;
183 }
184 }
185 queue!(
186 out,
187 style::SetForegroundColor(convert(style.fg)),
188 style::SetBackgroundColor(convert(style.bg))
189 )
190}