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