ad_editor/
term.rs

1//! Terminal TUI support.
2use crate::die;
3use libc::{
4    c_int, c_void, ioctl, sigaction, sighandler_t, siginfo_t, tcgetattr, tcsetattr,
5    termios as Termios, BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, ISIG, ISTRIP, IXON, OPOST,
6    SA_SIGINFO, SIGWINCH, STDOUT_FILENO, TCSAFLUSH, TIOCGWINSZ, VMIN, VTIME,
7};
8use serde::{Deserialize, Serialize};
9use std::{
10    fmt,
11    io::{self, Stdout, 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";
23pub const RESET_STYLE: &str = "\x1b[m";
24
25/// Used for storing and checking whether or not we've received a signal that our window
26/// size has changed.
27static WIN_SIZE_CHANGED: AtomicBool = AtomicBool::new(false);
28
29extern "C" fn handle_win_size_change(_: c_int, _: *mut siginfo_t, _: *mut c_void) {
30    WIN_SIZE_CHANGED.store(true, Ordering::Relaxed)
31}
32
33#[inline]
34pub(crate) fn win_size_changed() -> bool {
35    WIN_SIZE_CHANGED.swap(false, Ordering::Relaxed)
36}
37
38/// # Safety
39/// must only be called once
40pub unsafe fn register_signal_handler() {
41    let mut maybe_sa = mem::MaybeUninit::<sigaction>::uninit();
42    if libc::sigemptyset(&mut (*maybe_sa.as_mut_ptr()).sa_mask) == -1 {
43        die!(
44            "Unable to register signal handler: {}",
45            io::Error::last_os_error()
46        )
47    }
48
49    let mut sa_ptr = *maybe_sa.as_mut_ptr();
50    sa_ptr.sa_sigaction = handle_win_size_change as sighandler_t;
51    sa_ptr.sa_flags = SA_SIGINFO;
52
53    if libc::sigaction(SIGWINCH, &sa_ptr as *const _, ptr::null_mut()) == -1 {
54        die!(
55            "Unable to register signal handler: {}",
56            io::Error::last_os_error()
57        )
58    }
59}
60
61#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(try_from = "String", into = "String")]
63pub struct Color {
64    r: u8,
65    g: u8,
66    b: u8,
67}
68
69impl Color {
70    pub fn as_rgb_hex_string(&self) -> String {
71        let rgb: u32 = ((self.r as u32) << 16) + ((self.g as u32) << 8) + self.b as u32;
72        format!("#{:0>6X}", rgb)
73    }
74}
75
76impl From<Color> for String {
77    fn from(value: Color) -> Self {
78        value.as_rgb_hex_string()
79    }
80}
81
82impl TryFrom<&str> for Color {
83    type Error = String;
84
85    fn try_from(s: &str) -> Result<Self, String> {
86        let [_, r, g, b] = match u32::from_str_radix(s.strip_prefix('#').unwrap_or(s), 16) {
87            Ok(hex) => hex.to_be_bytes(),
88            Err(e) => return Err(format!("invalid color ('{s}'): {e}")),
89        };
90
91        Ok(Self { r, g, b })
92    }
93}
94
95impl TryFrom<String> for Color {
96    type Error = String;
97
98    fn try_from(value: String) -> Result<Self, Self::Error> {
99        Self::try_from(value.as_str())
100    }
101}
102
103#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104pub struct Styles {
105    #[serde(default)]
106    pub fg: Option<Color>,
107    #[serde(default)]
108    pub bg: Option<Color>,
109    #[serde(default)]
110    pub bold: bool,
111    #[serde(default)]
112    pub italic: bool,
113    #[serde(default)]
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(crate) 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
247pub(crate) fn clear_screen(stdout: &mut Stdout) {
248    if let Err(e) = stdout.write_all(format!("{CLEAR_SCREEN}{}", Cursor::ToStart).as_bytes()) {
249        panic!("unable to clear screen: {e}");
250    }
251    if let Err(e) = stdout.flush() {
252        panic!("unable to clear screen: {e}");
253    }
254}
255
256pub(crate) fn enable_mouse_support(stdout: &mut Stdout) {
257    if let Err(e) = stdout.write_all(ENABLE_MOUSE_SUPPORT.as_bytes()) {
258        panic!("unable to enable mouse support: {e}");
259    }
260    if let Err(e) = stdout.flush() {
261        panic!("unable to enable mouse support: {e}");
262    }
263}
264
265pub(crate) fn disable_mouse_support(stdout: &mut Stdout) {
266    if let Err(e) = stdout.write_all(DISABLE_MOUSE_SUPPORT.as_bytes()) {
267        panic!("unable to disable mouse support: {e}");
268    }
269    if let Err(e) = stdout.flush() {
270        panic!("unable to disable mouse support: {e}");
271    }
272}
273
274pub(crate) fn enable_alternate_screen(stdout: &mut Stdout) {
275    if let Err(e) = stdout.write_all(ENABLE_ALTERNATE_SCREEN.as_bytes()) {
276        panic!("unable to enable alternate screen: {e}");
277    }
278    if let Err(e) = stdout.flush() {
279        panic!("unable to enable alternate screen: {e}");
280    }
281}
282
283pub(crate) fn disable_alternate_screen(stdout: &mut Stdout) {
284    if let Err(e) = stdout.write_all(DISABLE_ALTERNATE_SCREEN.as_bytes()) {
285        panic!("unable to disable alternate screen: {e}");
286    }
287    if let Err(e) = stdout.flush() {
288        panic!("unable to disable alternate screen: {e}");
289    }
290}
291
292pub(crate) fn enable_raw_mode(mut t: Termios) {
293    t.c_iflag &= !(BRKINT | ICRNL | ISTRIP | IXON);
294    t.c_oflag &= !OPOST;
295    t.c_cflag |= CS8;
296    t.c_lflag &= !(ECHO | ICANON | IEXTEN | ISIG);
297    t.c_cc[VMIN] = 0;
298    t.c_cc[VTIME] = 1;
299
300    set_termios(t);
301}
302
303pub(crate) fn set_termios(t: Termios) {
304    // SAFETY: t is a valid termios struct to use as a pointer here
305    if unsafe { tcsetattr(STDOUT_FILENO, TCSAFLUSH, &t) } == -1 {
306        die!("tcsetattr");
307    }
308}
309
310pub(crate) fn get_termios() -> Termios {
311    // SAFETY: passing a null pointer here is valid
312    unsafe {
313        let mut t: Termios = mem::zeroed();
314        if tcgetattr(STDOUT_FILENO, &mut t as *mut _) == -1 {
315            die!("tcgetattr");
316        }
317
318        t
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn color_roundtrip() {
328        let s = "#FF9E3B";
329        let c: Color = s.try_into().unwrap();
330
331        assert_eq!(c.as_rgb_hex_string(), s);
332    }
333}