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