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