ad_editor/
term.rs

1//! Terminal TUI support.
2use crate::die;
3use libc::{
4    BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, ISIG, ISTRIP, IXON, OPOST, SA_SIGINFO, SIGWINCH,
5    STDOUT_FILENO, TCSAFLUSH, TIOCGWINSZ, VMIN, VTIME, c_int, c_void, ioctl, sigaction,
6    sighandler_t, siginfo_t, tcgetattr, tcsetattr, termios as Termios,
7};
8use serde::Deserialize;
9use std::{
10    fmt,
11    io::{self, Write},
12    mem, ptr,
13    sync::atomic::{AtomicBool, Ordering},
14};
15
16// ANSI escape codes:
17//   https://vt100.net/docs/vt100-ug/chapter3.html
18const CLEAR_SCREEN: &str = "\x1b[2J";
19const ENABLE_MOUSE_SUPPORT: &str = "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h";
20const DISABLE_MOUSE_SUPPORT: &str = "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l";
21const ENABLE_ALTERNATE_SCREEN: &str = "\x1b[?1049h";
22const DISABLE_ALTERNATE_SCREEN: &str = "\x1b[?1049l";
23const ENABLE_BRACKETED_PASTE: &str = "\x1b[?2004h";
24const DISABLE_BRACKETED_PASTE: &str = "\x1b[?2004l";
25pub const RESET_STYLE: &str = "\x1b[m";
26
27/// Used for storing and checking whether or not we've received a signal that our window
28/// size has changed.
29static WIN_SIZE_CHANGED: AtomicBool = AtomicBool::new(false);
30
31extern "C" fn handle_win_size_change(_: c_int, _: *mut siginfo_t, _: *mut c_void) {
32    WIN_SIZE_CHANGED.store(true, Ordering::Relaxed)
33}
34
35#[inline]
36pub(crate) fn win_size_changed() -> bool {
37    WIN_SIZE_CHANGED.swap(false, Ordering::Relaxed)
38}
39
40/// # Safety
41/// must only be called once
42pub unsafe fn register_signal_handler() {
43    let mut maybe_sa = mem::MaybeUninit::<sigaction>::uninit();
44    // SAFETY: we are meeting the C API requirements around usage of null pointers
45    unsafe {
46        if libc::sigemptyset(&mut (*maybe_sa.as_mut_ptr()).sa_mask) == -1 {
47            die!(
48                "Unable to register signal handler: {}",
49                io::Error::last_os_error()
50            )
51        }
52
53        let mut sa_ptr = *maybe_sa.as_mut_ptr();
54        sa_ptr.sa_sigaction = handle_win_size_change as *const () as sighandler_t;
55        sa_ptr.sa_flags = SA_SIGINFO;
56
57        if libc::sigaction(SIGWINCH, &sa_ptr as *const _, ptr::null_mut()) == -1 {
58            die!(
59                "Unable to register signal handler: {}",
60                io::Error::last_os_error()
61            )
62        }
63    }
64}
65
66#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize)]
67#[serde(try_from = "String", into = "String")]
68pub struct Color {
69    r: u8,
70    g: u8,
71    b: u8,
72}
73
74impl Color {
75    pub fn as_rgb_hex_string(&self) -> String {
76        let rgb: u32 = ((self.r as u32) << 16) + ((self.g as u32) << 8) + self.b as u32;
77        format!("#{:0>6X}", rgb)
78    }
79}
80
81impl From<Color> for String {
82    fn from(value: Color) -> Self {
83        value.as_rgb_hex_string()
84    }
85}
86
87impl TryFrom<&str> for Color {
88    type Error = String;
89
90    fn try_from(s: &str) -> Result<Self, String> {
91        let [_, r, g, b] = match u32::from_str_radix(s.strip_prefix('#').unwrap_or(s), 16) {
92            Ok(hex) => hex.to_be_bytes(),
93            Err(e) => return Err(format!("invalid color ('{s}'): {e}")),
94        };
95
96        Ok(Self { r, g, b })
97    }
98}
99
100impl TryFrom<String> for Color {
101    type Error = String;
102
103    fn try_from(value: String) -> Result<Self, Self::Error> {
104        Self::try_from(value.as_str())
105    }
106}
107
108#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
109pub struct Styles {
110    pub fg: Option<Color>,
111    pub bg: Option<Color>,
112    pub bold: bool,
113    pub italic: bool,
114    pub underline: bool,
115}
116
117impl fmt::Display for Styles {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        if let Some(fg) = self.fg {
120            write!(f, "{}", Style::Fg(fg))?;
121        }
122        if let Some(bg) = self.bg {
123            write!(f, "{}", Style::Bg(bg))?;
124        }
125        if self.bold {
126            write!(f, "{}", Style::Bold)?;
127        }
128        if self.italic {
129            write!(f, "{}", Style::Italic)?;
130        }
131        if self.underline {
132            write!(f, "{}", Style::Underline)?;
133        }
134
135        Ok(())
136    }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum Style {
141    Fg(Color),
142    Bg(Color),
143    Bold,
144    NoBold,
145    Italic,
146    NoItalic,
147    Underline,
148    NoUnderline,
149    Reverse,
150    NoReverse,
151    Reset,
152}
153
154// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#8-16-colors
155impl fmt::Display for Style {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        use Style::*;
158
159        match self {
160            Fg(Color { r, b, g }) => write!(f, "\x1b[38;2;{r};{g};{b}m"),
161            Bg(Color { r, b, g }) => write!(f, "\x1b[48;2;{r};{g};{b}m"),
162            Bold => write!(f, "\x1b[1m"),
163            NoBold => write!(f, "\x1b[22m"),
164            Italic => write!(f, "\x1b[3m"),
165            NoItalic => write!(f, "\x1b[23m"),
166            Underline => write!(f, "\x1b[4m"),
167            NoUnderline => write!(f, "\x1b[24m"),
168            Reverse => write!(f, "\x1b[7m"),
169            NoReverse => write!(f, "\x1b[27m"),
170            Reset => write!(f, "\x1b[m"),
171        }
172    }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub(crate) enum Cursor {
177    To(usize, usize),
178    ToStart,
179    Hide,
180    Show,
181    ClearRight,
182}
183
184impl fmt::Display for Cursor {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        use Cursor::*;
187
188        match self {
189            To(x, y) => write!(f, "\x1b[{y};{x}H"),
190            ToStart => write!(f, "\x1b[H"),
191            Hide => write!(f, "\x1b[?25l"),
192            Show => write!(f, "\x1b[?25h"),
193            ClearRight => write!(f, "\x1b[K"),
194        }
195    }
196}
197
198#[allow(dead_code)]
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum CurShape {
201    Block,
202    Bar,
203    Underline,
204    BlinkingBlock,
205    BlinkingBar,
206    BlinkingUnderline,
207}
208
209impl fmt::Display for CurShape {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        use CurShape::*;
212
213        match self {
214            BlinkingBlock => write!(f, "\x1b[\x31 q"),
215            Block => write!(f, "\x1b[\x32 q"),
216            BlinkingUnderline => write!(f, "\x1b[\x33 q"),
217            Underline => write!(f, "\x1b[\x34 q"),
218            BlinkingBar => write!(f, "\x1b[\x35 q"),
219            Bar => write!(f, "\x1b[\x36 q"),
220        }
221    }
222}
223
224/// Request the current terminal size from the kernel using ioctl
225pub(crate) fn get_termsize() -> (usize, usize) {
226    #[repr(C)]
227    struct Termsize {
228        r: u16,
229        c: u16,
230        x: u16,
231        y: u16,
232    }
233
234    let mut ts = Termsize {
235        r: 0,
236        c: 0,
237        x: 0,
238        y: 0,
239    };
240
241    // SAFETY: ts is a valid termsize struct to pass as a pointer here
242    unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut ts as *mut _) };
243
244    (ts.r as usize, ts.c as usize)
245}
246
247#[inline]
248fn write_control_seq(seq: &str, desc: &str, stdout: &mut impl Write) {
249    if let Err(e) = stdout.write_all(seq.as_bytes()) {
250        panic!("unable to {desc}: {e}");
251    }
252    if let Err(e) = stdout.flush() {
253        panic!("unable to {desc}: {e}");
254    }
255}
256
257pub(crate) fn clear_screen(stdout: &mut impl Write) {
258    write_control_seq(
259        &format!("{CLEAR_SCREEN}{}", Cursor::ToStart),
260        "clear screen",
261        stdout,
262    )
263}
264
265pub(crate) fn enable_mouse_support(stdout: &mut impl Write) {
266    write_control_seq(ENABLE_MOUSE_SUPPORT, "enable mouse support", stdout)
267}
268
269pub(crate) fn disable_mouse_support(stdout: &mut impl Write) {
270    write_control_seq(DISABLE_MOUSE_SUPPORT, "disable mouse support", stdout)
271}
272
273pub(crate) fn enable_alternate_screen(stdout: &mut impl Write) {
274    write_control_seq(ENABLE_ALTERNATE_SCREEN, "enable alternate screen", stdout)
275}
276
277pub(crate) fn disable_alternate_screen(stdout: &mut impl Write) {
278    write_control_seq(DISABLE_ALTERNATE_SCREEN, "disable alternate screen", stdout)
279}
280
281pub(crate) fn enable_bracketed_paste(stdout: &mut impl Write) {
282    write_control_seq(ENABLE_BRACKETED_PASTE, "enable bracketed paste", stdout)
283}
284
285pub(crate) fn disable_bracketed_paste(stdout: &mut impl Write) {
286    write_control_seq(DISABLE_BRACKETED_PASTE, "disable bracketed paste", stdout)
287}
288
289pub(crate) fn enable_raw_mode(mut t: Termios) {
290    t.c_iflag &= !(BRKINT | ICRNL | ISTRIP | IXON);
291    t.c_oflag &= !OPOST;
292    t.c_cflag |= CS8;
293    t.c_lflag &= !(ECHO | ICANON | IEXTEN | ISIG);
294    t.c_cc[VMIN] = 0;
295    t.c_cc[VTIME] = 1;
296
297    set_termios(t);
298}
299
300pub(crate) fn set_termios(t: Termios) {
301    // SAFETY: t is a valid termios struct to use as a pointer here
302    if unsafe { tcsetattr(STDOUT_FILENO, TCSAFLUSH, &t) } == -1 {
303        die!("tcsetattr");
304    }
305}
306
307pub(crate) fn get_termios() -> Termios {
308    // SAFETY: passing a null pointer here is valid
309    unsafe {
310        let mut t: Termios = mem::zeroed();
311        if tcgetattr(STDOUT_FILENO, &mut t as *mut _) == -1 {
312            die!("tcgetattr");
313        }
314
315        t
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn color_roundtrip() {
325        let s = "#FF9E3B";
326        let c: Color = s.try_into().unwrap();
327
328        assert_eq!(c.as_rgb_hex_string(), s);
329    }
330}