1use 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
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";
23pub const RESET_STYLE: &str = "\x1b[m";
24
25static 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
38pub 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
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(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
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
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 if unsafe { tcsetattr(STDOUT_FILENO, TCSAFLUSH, &t) } == -1 {
306 die!("tcsetattr");
307 }
308}
309
310pub(crate) fn get_termios() -> Termios {
311 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}