1use std::env;
2use std::fmt::Display;
3use std::fs;
4use std::io;
5use std::io::{BufRead, BufReader};
6use std::mem;
7use std::os::unix::io::AsRawFd;
8use std::str;
9
10#[cfg(not(target_os = "macos"))]
11use once_cell::sync::Lazy;
12
13use crate::kb::Key;
14use crate::term::Term;
15
16pub use crate::common_term::*;
17
18pub const DEFAULT_WIDTH: u16 = 80;
19
20#[inline]
21pub fn is_a_terminal(out: &Term) -> bool {
22 unsafe { libc::isatty(out.as_raw_fd()) != 0 }
23}
24
25pub fn is_a_color_terminal(out: &Term) -> bool {
26 if !is_a_terminal(out) {
27 return false;
28 }
29
30 if env::var("NO_COLOR").is_ok() {
31 return false;
32 }
33
34 match env::var("TERM") {
35 Ok(term) => term != "dumb",
36 Err(_) => false,
37 }
38}
39
40pub fn c_result<F: FnOnce() -> libc::c_int>(f: F) -> io::Result<()> {
41 let res = f();
42 if res != 0 {
43 Err(io::Error::last_os_error())
44 } else {
45 Ok(())
46 }
47}
48
49pub fn terminal_size(out: &Term) -> Option<(u16, u16)> {
50 unsafe {
51 if libc::isatty(out.as_raw_fd()) != 1 {
52 return None;
53 }
54
55 let mut winsize: libc::winsize = mem::zeroed();
56
57 #[allow(clippy::useless_conversion)]
60 libc::ioctl(out.as_raw_fd(), libc::TIOCGWINSZ.into(), &mut winsize);
61 if winsize.ws_row > 0 && winsize.ws_col > 0 {
62 Some((winsize.ws_row as u16, winsize.ws_col as u16))
63 } else {
64 None
65 }
66 }
67}
68
69pub fn read_secure() -> io::Result<String> {
70 let f_tty;
71 let fd = unsafe {
72 if libc::isatty(libc::STDIN_FILENO) == 1 {
73 f_tty = None;
74 libc::STDIN_FILENO
75 } else {
76 let f = fs::OpenOptions::new()
77 .read(true)
78 .write(true)
79 .open("/dev/tty")?;
80 let fd = f.as_raw_fd();
81 f_tty = Some(BufReader::new(f));
82 fd
83 }
84 };
85
86 let mut termios = mem::MaybeUninit::uninit();
87 c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?;
88 let mut termios = unsafe { termios.assume_init() };
89 let original = termios;
90 termios.c_lflag &= !libc::ECHO;
91 c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) })?;
92 let mut rv = String::new();
93
94 let read_rv = if let Some(mut f) = f_tty {
95 f.read_line(&mut rv)
96 } else {
97 io::stdin().read_line(&mut rv)
98 };
99
100 c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &original) })?;
101
102 read_rv.map(|_| {
103 let len = rv.trim_end_matches(&['\r', '\n'][..]).len();
104 rv.truncate(len);
105 rv
106 })
107}
108
109fn poll_fd(fd: i32, timeout: i32) -> io::Result<bool> {
110 let mut pollfd = libc::pollfd {
111 fd,
112 events: libc::POLLIN,
113 revents: 0,
114 };
115 let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) };
116 if ret < 0 {
117 Err(io::Error::last_os_error())
118 } else {
119 Ok(pollfd.revents & libc::POLLIN != 0)
120 }
121}
122
123#[cfg(target_os = "macos")]
124fn select_fd(fd: i32, timeout: i32) -> io::Result<bool> {
125 unsafe {
126 let mut read_fd_set: libc::fd_set = mem::zeroed();
127
128 let mut timeout_val;
129 let timeout = if timeout < 0 {
130 std::ptr::null_mut()
131 } else {
132 timeout_val = libc::timeval {
133 tv_sec: (timeout / 1000) as _,
134 tv_usec: (timeout * 1000) as _,
135 };
136 &mut timeout_val
137 };
138
139 libc::FD_ZERO(&mut read_fd_set);
140 libc::FD_SET(fd, &mut read_fd_set);
141 let ret = libc::select(
142 fd + 1,
143 &mut read_fd_set,
144 std::ptr::null_mut(),
145 std::ptr::null_mut(),
146 timeout,
147 );
148 if ret < 0 {
149 Err(io::Error::last_os_error())
150 } else {
151 Ok(libc::FD_ISSET(fd, &read_fd_set))
152 }
153 }
154}
155
156fn select_or_poll_term_fd(fd: i32, timeout: i32) -> io::Result<bool> {
157 #[cfg(target_os = "macos")]
161 {
162 if unsafe { libc::isatty(fd) == 1 } {
163 return select_fd(fd, timeout);
164 }
165 }
166 poll_fd(fd, timeout)
167}
168
169fn read_single_char(fd: i32) -> io::Result<Option<char>> {
170 let is_ready = select_or_poll_term_fd(fd, 0)?;
172
173 if is_ready {
174 let mut buf: [u8; 1] = [0];
176
177 read_bytes(fd, &mut buf, 1)?;
178 Ok(Some(buf[0] as char))
179 } else {
180 Ok(None)
182 }
183}
184
185fn read_bytes(fd: i32, buf: &mut [u8], count: u8) -> io::Result<u8> {
189 let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) };
190 if read < 0 {
191 Err(io::Error::last_os_error())
192 } else if read == 0 {
193 Err(io::Error::new(
194 io::ErrorKind::UnexpectedEof,
195 "Reached end of file",
196 ))
197 } else if buf[0] == b'\x03' {
198 Err(io::Error::new(
199 io::ErrorKind::Interrupted,
200 "read interrupted",
201 ))
202 } else {
203 Ok(read as u8)
204 }
205}
206
207fn read_single_key_impl(fd: i32) -> Result<Key, io::Error> {
208 loop {
209 match read_single_char(fd)? {
210 Some('\x1b') => {
211 break if let Some(c1) = read_single_char(fd)? {
213 if c1 == '[' {
214 if let Some(c2) = read_single_char(fd)? {
215 match c2 {
216 'A' => Ok(Key::ArrowUp),
217 'B' => Ok(Key::ArrowDown),
218 'C' => Ok(Key::ArrowRight),
219 'D' => Ok(Key::ArrowLeft),
220 'H' => Ok(Key::Home),
221 'F' => Ok(Key::End),
222 'Z' => Ok(Key::BackTab),
223 _ => {
224 let c3 = read_single_char(fd)?;
225 if let Some(c3) = c3 {
226 if c3 == '~' {
227 match c2 {
228 '1' => Ok(Key::Home), '2' => Ok(Key::Insert),
230 '3' => Ok(Key::Del),
231 '4' => Ok(Key::End), '5' => Ok(Key::PageUp),
233 '6' => Ok(Key::PageDown),
234 '7' => Ok(Key::Home), '8' => Ok(Key::End), _ => Ok(Key::UnknownEscSeq(vec![c1, c2, c3])),
237 }
238 } else {
239 Ok(Key::UnknownEscSeq(vec![c1, c2, c3]))
240 }
241 } else {
242 Ok(Key::UnknownEscSeq(vec![c1, c2]))
244 }
245 }
246 }
247 } else {
248 Ok(Key::UnknownEscSeq(vec![c1]))
250 }
251 } else {
252 Ok(Key::UnknownEscSeq(vec![c1]))
254 }
255 } else {
256 Ok(Key::Escape)
258 };
259 }
260 Some(c) => {
261 let byte = c as u8;
262 let mut buf: [u8; 4] = [byte, 0, 0, 0];
263
264 break if byte & 224u8 == 192u8 {
265 read_bytes(fd, &mut buf[1..], 1)?;
267 Ok(key_from_utf8(&buf[..2]))
268 } else if byte & 240u8 == 224u8 {
269 read_bytes(fd, &mut buf[1..], 2)?;
271 Ok(key_from_utf8(&buf[..3]))
272 } else if byte & 248u8 == 240u8 {
273 read_bytes(fd, &mut buf[1..], 3)?;
275 Ok(key_from_utf8(&buf[..4]))
276 } else {
277 Ok(match c {
278 '\n' | '\r' => Key::Enter,
279 '\x7f' => Key::Backspace,
280 '\t' => Key::Tab,
281 '\x01' => Key::Home, '\x05' => Key::End, '\x08' => Key::Backspace, _ => Key::Char(c),
285 })
286 };
287 }
288 None => {
289 match select_or_poll_term_fd(fd, -1) {
292 Ok(_) => continue,
293 Err(_) => break Err(io::Error::last_os_error()),
294 }
295 }
296 }
297 }
298}
299
300pub fn read_single_key(ctrlc_key: bool) -> io::Result<Key> {
301 let tty_f;
302 let fd = unsafe {
303 if libc::isatty(libc::STDIN_FILENO) == 1 {
304 libc::STDIN_FILENO
305 } else {
306 tty_f = fs::OpenOptions::new()
307 .read(true)
308 .write(true)
309 .open("/dev/tty")?;
310 tty_f.as_raw_fd()
311 }
312 };
313 let mut termios = core::mem::MaybeUninit::uninit();
314 c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?;
315 let mut termios = unsafe { termios.assume_init() };
316 let original = termios;
317 unsafe { libc::cfmakeraw(&mut termios) };
318 termios.c_oflag = original.c_oflag;
319 c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &termios) })?;
320 let rv: io::Result<Key> = read_single_key_impl(fd);
321 c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &original) })?;
322
323 if let Err(ref err) = rv {
325 if err.kind() == io::ErrorKind::Interrupted {
326 if !ctrlc_key {
327 unsafe {
328 libc::raise(libc::SIGINT);
329 }
330 } else {
331 return Ok(Key::CtrlC);
332 }
333 }
334 }
335
336 rv
337}
338
339pub fn key_from_utf8(buf: &[u8]) -> Key {
340 if let Ok(s) = str::from_utf8(buf) {
341 if let Some(c) = s.chars().next() {
342 return Key::Char(c);
343 }
344 }
345 Key::Unknown
346}
347
348#[cfg(not(target_os = "macos"))]
349static IS_LANG_UTF8: Lazy<bool> = Lazy::new(|| match std::env::var("LANG") {
350 Ok(lang) => lang.to_uppercase().ends_with("UTF-8"),
351 _ => false,
352});
353
354#[cfg(target_os = "macos")]
355pub fn wants_emoji() -> bool {
356 true
357}
358
359#[cfg(not(target_os = "macos"))]
360pub fn wants_emoji() -> bool {
361 *IS_LANG_UTF8
362}
363
364pub fn set_title<T: Display>(title: T) {
365 print!("\x1b]0;{}\x07", title);
366}