Skip to main content

taba_cli/tui/
term.rs

1//! The real terminal, in raw mode and with no library: `stty` on Linux and macOS, the console's
2//! `SetConsoleMode` on Windows (with virtual sequences, Windows 10 and later).
3
4use std::collections::VecDeque;
5use std::io::{self, Read, Write};
6
7use super::keys::{self, Key};
8use super::{Frame, Terminal};
9
10pub struct RawTerminal {
11    pending: VecDeque<Key>,
12    color: bool,
13    saved: sys::Saved,
14}
15
16impl RawTerminal {
17    /// Puts the terminal in raw mode and enters the alternate screen. Fails if the input or output are not
18    /// a terminal.
19    pub fn open() -> io::Result<RawTerminal> {
20        let saved = sys::enter_raw()?;
21        let mut out = io::stdout();
22        out.write_all(b"\x1b[?1049h\x1b[?25l")?;
23        out.flush()?;
24        Ok(RawTerminal {
25            pending: VecDeque::new(),
26            color: std::env::var_os("NO_COLOR").is_none_or(|v| v.is_empty()),
27            saved,
28        })
29    }
30}
31
32impl Drop for RawTerminal {
33    fn drop(&mut self) {
34        let mut out = io::stdout();
35        let _ = out.write_all(b"\x1b[0m\x1b[?25h\x1b[?1049l");
36        let _ = out.flush();
37        sys::restore(&self.saved);
38    }
39}
40
41impl Terminal for RawTerminal {
42    fn size(&self) -> (usize, usize) {
43        sys::size().unwrap_or((80, 24))
44    }
45
46    fn read_key(&mut self) -> io::Result<Option<Key>> {
47        loop {
48            if let Some(key) = self.pending.pop_front() {
49                return Ok(Some(key));
50            }
51            let mut buf = [0u8; 256];
52            let n = io::stdin().lock().read(&mut buf)?;
53            if n == 0 {
54                return Ok(None);
55            }
56            self.pending.extend(keys::decode(&buf[..n]));
57        }
58    }
59
60    fn draw(&mut self, frame: &Frame) -> io::Result<()> {
61        let mut out = String::new();
62        out.push_str("\x1b[?25l");
63        for (row, line) in frame.lines.iter().enumerate() {
64            out.push_str(&format!("\x1b[{};1H{line}\x1b[0m\x1b[K", row + 1));
65        }
66        out.push_str(&format!("\x1b[{};1H\x1b[J", frame.lines.len() + 1));
67        if let Some((row, col)) = frame.cursor {
68            out.push_str(&format!("\x1b[{};{}H\x1b[?25h", row + 1, col + 1));
69        }
70        let mut stdout = io::stdout();
71        stdout.write_all(out.as_bytes())?;
72        stdout.flush()
73    }
74
75    fn color(&self) -> bool {
76        self.color
77    }
78}
79
80#[cfg(unix)]
81mod sys {
82    use std::io;
83    use std::process::{Command, Stdio};
84
85    pub struct Saved(String);
86
87    fn stty(args: &[&str]) -> io::Result<String> {
88        let out = Command::new("stty")
89            .args(args)
90            .stdin(Stdio::inherit())
91            .stderr(Stdio::null())
92            .output()?;
93        if !out.status.success() {
94            return Err(io::Error::other("stty failed (is the input a terminal?)"));
95        }
96        Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
97    }
98
99    pub fn enter_raw() -> io::Result<Saved> {
100        let saved = stty(&["-g"])?;
101        stty(&["raw", "-echo"])?;
102        Ok(Saved(saved))
103    }
104
105    pub fn restore(saved: &Saved) {
106        let _ = stty(&[&saved.0]);
107    }
108
109    pub fn size() -> Option<(usize, usize)> {
110        let text = stty(&["size"]).ok()?;
111        let mut parts = text.split_whitespace();
112        let rows: usize = parts.next()?.parse().ok()?;
113        let cols: usize = parts.next()?.parse().ok()?;
114        Some((cols, rows))
115    }
116}
117
118#[cfg(windows)]
119mod sys {
120    use std::io;
121
122    type Handle = isize;
123
124    #[repr(C)]
125    #[derive(Default, Clone, Copy)]
126    struct Coord {
127        x: i16,
128        y: i16,
129    }
130
131    #[repr(C)]
132    #[derive(Default, Clone, Copy)]
133    struct SmallRect {
134        left: i16,
135        top: i16,
136        right: i16,
137        bottom: i16,
138    }
139
140    #[repr(C)]
141    #[derive(Default, Clone, Copy)]
142    struct ScreenBufferInfo {
143        size: Coord,
144        cursor: Coord,
145        attributes: u16,
146        window: SmallRect,
147        maximum: Coord,
148    }
149
150    #[link(name = "kernel32")]
151    extern "system" {
152        fn GetStdHandle(which: u32) -> Handle;
153        fn GetConsoleMode(handle: Handle, mode: *mut u32) -> i32;
154        fn SetConsoleMode(handle: Handle, mode: u32) -> i32;
155        fn GetConsoleScreenBufferInfo(handle: Handle, info: *mut ScreenBufferInfo) -> i32;
156        fn GetConsoleOutputCP() -> u32;
157        fn SetConsoleOutputCP(code_page: u32) -> i32;
158    }
159
160    const STD_INPUT: u32 = -10i32 as u32;
161    const STD_OUTPUT: u32 = -11i32 as u32;
162    const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
163    const ENABLE_LINE_INPUT: u32 = 0x0002;
164    const ENABLE_ECHO_INPUT: u32 = 0x0004;
165    const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
166    const ENABLE_PROCESSED_OUTPUT: u32 = 0x0001;
167    const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
168    const UTF8: u32 = 65001;
169
170    pub struct Saved {
171        input: u32,
172        output: u32,
173        code_page: u32,
174    }
175
176    fn mode(handle: Handle) -> io::Result<u32> {
177        let mut mode = 0;
178        // SAFETY: `handle` comes from `GetStdHandle` and `mode` is a `u32` valid for writing.
179        if unsafe { GetConsoleMode(handle, &mut mode) } == 0 {
180            return Err(io::Error::last_os_error());
181        }
182        Ok(mode)
183    }
184
185    fn set_mode(handle: Handle, mode: u32) -> io::Result<()> {
186        // SAFETY: only passes the value to the console.
187        if unsafe { SetConsoleMode(handle, mode) } == 0 {
188            return Err(io::Error::last_os_error());
189        }
190        Ok(())
191    }
192
193    pub fn enter_raw() -> io::Result<Saved> {
194        // SAFETY: `GetStdHandle` and `GetConsoleOutputCP` only read process state.
195        let (input, output, code_page) = unsafe {
196            (
197                GetStdHandle(STD_INPUT),
198                GetStdHandle(STD_OUTPUT),
199                GetConsoleOutputCP(),
200            )
201        };
202        let saved = Saved {
203            input: mode(input)?,
204            output: mode(output)?,
205            code_page,
206        };
207        set_mode(
208            input,
209            (saved.input & !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT))
210                | ENABLE_VIRTUAL_TERMINAL_INPUT,
211        )?;
212        if let Err(e) = set_mode(
213            output,
214            saved.output | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
215        ) {
216            let _ = set_mode(input, saved.input);
217            return Err(e);
218        }
219        // SAFETY: only changes the output code page, restored in `restore`.
220        unsafe { SetConsoleOutputCP(UTF8) };
221        Ok(saved)
222    }
223
224    pub fn restore(saved: &Saved) {
225        // SAFETY: gives the console back the values it had.
226        unsafe {
227            let _ = SetConsoleMode(GetStdHandle(STD_INPUT), saved.input);
228            let _ = SetConsoleMode(GetStdHandle(STD_OUTPUT), saved.output);
229            SetConsoleOutputCP(saved.code_page);
230        }
231    }
232
233    pub fn size() -> Option<(usize, usize)> {
234        let mut info = ScreenBufferInfo::default();
235        // SAFETY: `info` is a structure valid for writing, in the format the console expects.
236        let ok = unsafe { GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT), &mut info) };
237        if ok == 0 {
238            return None;
239        }
240        let w = (info.window.right - info.window.left + 1) as usize;
241        let h = (info.window.bottom - info.window.top + 1) as usize;
242        Some((w, h))
243    }
244}
245
246#[cfg(not(any(unix, windows)))]
247mod sys {
248    use std::io;
249
250    pub struct Saved;
251
252    pub fn enter_raw() -> io::Result<Saved> {
253        Err(io::Error::new(
254            io::ErrorKind::Unsupported,
255            "this system does not have the interactive screen",
256        ))
257    }
258
259    pub fn restore(_: &Saved) {}
260
261    pub fn size() -> Option<(usize, usize)> {
262        None
263    }
264}