1use 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
16const 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
27static 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
40pub unsafe fn register_signal_handler() {
43 let mut maybe_sa = mem::MaybeUninit::<sigaction>::uninit();
44 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
154impl 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
224pub(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 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 if unsafe { tcsetattr(STDOUT_FILENO, TCSAFLUSH, &t) } == -1 {
303 die!("tcsetattr");
304 }
305}
306
307pub(crate) fn get_termios() -> Termios {
308 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}