1 | use std::fmt::{Debug, Display}; |
2 | use std::io::{self, Read, Write}; |
3 | use std::sync::{Arc, Mutex}; |
4 | |
5 | #[cfg (unix)] |
6 | use std::os::unix::io::{AsRawFd, RawFd}; |
7 | #[cfg (windows)] |
8 | use std::os::windows::io::{AsRawHandle, RawHandle}; |
9 | |
10 | use crate::{kb::Key, utils::Style}; |
11 | |
12 | #[cfg (unix)] |
13 | trait TermWrite: Write + Debug + AsRawFd + Send {} |
14 | #[cfg (unix)] |
15 | impl<T: Write + Debug + AsRawFd + Send> TermWrite for T {} |
16 | |
17 | #[cfg (unix)] |
18 | trait TermRead: Read + Debug + AsRawFd + Send {} |
19 | #[cfg (unix)] |
20 | impl<T: Read + Debug + AsRawFd + Send> TermRead for T {} |
21 | |
22 | #[cfg (unix)] |
23 | #[derive (Debug, Clone)] |
24 | pub struct ReadWritePair { |
25 | #[allow (unused)] |
26 | read: Arc<Mutex<dyn TermRead>>, |
27 | write: Arc<Mutex<dyn TermWrite>>, |
28 | style: Style, |
29 | } |
30 | |
31 | /// Where the term is writing. |
32 | #[derive (Debug, Clone)] |
33 | pub enum TermTarget { |
34 | Stdout, |
35 | Stderr, |
36 | #[cfg (unix)] |
37 | ReadWritePair(ReadWritePair), |
38 | } |
39 | |
40 | #[derive (Debug)] |
41 | pub struct TermInner { |
42 | target: TermTarget, |
43 | buffer: Option<Mutex<Vec<u8>>>, |
44 | } |
45 | |
46 | /// The family of the terminal. |
47 | #[derive (Debug, Copy, Clone, PartialEq, Eq)] |
48 | pub enum TermFamily { |
49 | /// Redirected to a file or file like thing. |
50 | File, |
51 | /// A standard unix terminal. |
52 | UnixTerm, |
53 | /// A cmd.exe like windows console. |
54 | WindowsConsole, |
55 | /// A dummy terminal (for instance on wasm) |
56 | Dummy, |
57 | } |
58 | |
59 | /// Gives access to the terminal features. |
60 | #[derive (Debug, Clone)] |
61 | pub struct TermFeatures<'a>(&'a Term); |
62 | |
63 | impl<'a> TermFeatures<'a> { |
64 | /// Check if this is a real user attended terminal (`isatty`) |
65 | #[inline ] |
66 | pub fn is_attended(&self) -> bool { |
67 | is_a_terminal(self.0) |
68 | } |
69 | |
70 | /// Check if colors are supported by this terminal. |
71 | /// |
72 | /// This does not check if colors are enabled. Currently all terminals |
73 | /// are considered to support colors |
74 | #[inline ] |
75 | pub fn colors_supported(&self) -> bool { |
76 | is_a_color_terminal(self.0) |
77 | } |
78 | |
79 | /// Check if this terminal is an msys terminal. |
80 | /// |
81 | /// This is sometimes useful to disable features that are known to not |
82 | /// work on msys terminals or require special handling. |
83 | #[inline ] |
84 | pub fn is_msys_tty(&self) -> bool { |
85 | #[cfg (windows)] |
86 | { |
87 | msys_tty_on(self.0) |
88 | } |
89 | #[cfg (not(windows))] |
90 | { |
91 | false |
92 | } |
93 | } |
94 | |
95 | /// Check if this terminal wants emojis. |
96 | #[inline ] |
97 | pub fn wants_emoji(&self) -> bool { |
98 | self.is_attended() && wants_emoji() |
99 | } |
100 | |
101 | /// Return the family of the terminal. |
102 | #[inline ] |
103 | pub fn family(&self) -> TermFamily { |
104 | if !self.is_attended() { |
105 | return TermFamily::File; |
106 | } |
107 | #[cfg (windows)] |
108 | { |
109 | TermFamily::WindowsConsole |
110 | } |
111 | #[cfg (unix)] |
112 | { |
113 | TermFamily::UnixTerm |
114 | } |
115 | #[cfg (target_arch = "wasm32" )] |
116 | { |
117 | TermFamily::Dummy |
118 | } |
119 | } |
120 | } |
121 | |
122 | /// Abstraction around a terminal. |
123 | /// |
124 | /// A terminal can be cloned. If a buffer is used it's shared across all |
125 | /// clones which means it largely acts as a handle. |
126 | #[derive (Clone, Debug)] |
127 | pub struct Term { |
128 | inner: Arc<TermInner>, |
129 | pub(crate) is_msys_tty: bool, |
130 | pub(crate) is_tty: bool, |
131 | } |
132 | |
133 | impl Term { |
134 | fn with_inner(inner: TermInner) -> Term { |
135 | let mut term = Term { |
136 | inner: Arc::new(inner), |
137 | is_msys_tty: false, |
138 | is_tty: false, |
139 | }; |
140 | |
141 | term.is_msys_tty = term.features().is_msys_tty(); |
142 | term.is_tty = term.features().is_attended(); |
143 | term |
144 | } |
145 | |
146 | /// Return a new unbuffered terminal. |
147 | #[inline ] |
148 | pub fn stdout() -> Term { |
149 | Term::with_inner(TermInner { |
150 | target: TermTarget::Stdout, |
151 | buffer: None, |
152 | }) |
153 | } |
154 | |
155 | /// Return a new unbuffered terminal to stderr. |
156 | #[inline ] |
157 | pub fn stderr() -> Term { |
158 | Term::with_inner(TermInner { |
159 | target: TermTarget::Stderr, |
160 | buffer: None, |
161 | }) |
162 | } |
163 | |
164 | /// Return a new buffered terminal. |
165 | pub fn buffered_stdout() -> Term { |
166 | Term::with_inner(TermInner { |
167 | target: TermTarget::Stdout, |
168 | buffer: Some(Mutex::new(vec![])), |
169 | }) |
170 | } |
171 | |
172 | /// Return a new buffered terminal to stderr. |
173 | pub fn buffered_stderr() -> Term { |
174 | Term::with_inner(TermInner { |
175 | target: TermTarget::Stderr, |
176 | buffer: Some(Mutex::new(vec![])), |
177 | }) |
178 | } |
179 | |
180 | /// Return a terminal for the given Read/Write pair styled like stderr. |
181 | #[cfg (unix)] |
182 | pub fn read_write_pair<R, W>(read: R, write: W) -> Term |
183 | where |
184 | R: Read + Debug + AsRawFd + Send + 'static, |
185 | W: Write + Debug + AsRawFd + Send + 'static, |
186 | { |
187 | Self::read_write_pair_with_style(read, write, Style::new().for_stderr()) |
188 | } |
189 | |
190 | /// Return a terminal for the given Read/Write pair. |
191 | #[cfg (unix)] |
192 | pub fn read_write_pair_with_style<R, W>(read: R, write: W, style: Style) -> Term |
193 | where |
194 | R: Read + Debug + AsRawFd + Send + 'static, |
195 | W: Write + Debug + AsRawFd + Send + 'static, |
196 | { |
197 | Term::with_inner(TermInner { |
198 | target: TermTarget::ReadWritePair(ReadWritePair { |
199 | read: Arc::new(Mutex::new(read)), |
200 | write: Arc::new(Mutex::new(write)), |
201 | style, |
202 | }), |
203 | buffer: None, |
204 | }) |
205 | } |
206 | |
207 | /// Return the style for this terminal. |
208 | #[inline ] |
209 | pub fn style(&self) -> Style { |
210 | match self.inner.target { |
211 | TermTarget::Stderr => Style::new().for_stderr(), |
212 | TermTarget::Stdout => Style::new().for_stdout(), |
213 | #[cfg (unix)] |
214 | TermTarget::ReadWritePair(ReadWritePair { ref style, .. }) => style.clone(), |
215 | } |
216 | } |
217 | |
218 | /// Return the target of this terminal. |
219 | #[inline ] |
220 | pub fn target(&self) -> TermTarget { |
221 | self.inner.target.clone() |
222 | } |
223 | |
224 | #[doc (hidden)] |
225 | pub fn write_str(&self, s: &str) -> io::Result<()> { |
226 | match self.inner.buffer { |
227 | Some(ref buffer) => buffer.lock().unwrap().write_all(s.as_bytes()), |
228 | None => self.write_through(s.as_bytes()), |
229 | } |
230 | } |
231 | |
232 | /// Write a string to the terminal and add a newline. |
233 | pub fn write_line(&self, s: &str) -> io::Result<()> { |
234 | match self.inner.buffer { |
235 | Some(ref mutex) => { |
236 | let mut buffer = mutex.lock().unwrap(); |
237 | buffer.extend_from_slice(s.as_bytes()); |
238 | buffer.push(b' \n' ); |
239 | Ok(()) |
240 | } |
241 | None => self.write_through(format!(" {}\n" , s).as_bytes()), |
242 | } |
243 | } |
244 | |
245 | /// Read a single character from the terminal. |
246 | /// |
247 | /// This does not echo the character and blocks until a single character |
248 | /// or complete key chord is entered. If the terminal is not user attended |
249 | /// the return value will be an error. |
250 | pub fn read_char(&self) -> io::Result<char> { |
251 | if !self.is_tty { |
252 | return Err(io::Error::new( |
253 | io::ErrorKind::NotConnected, |
254 | "Not a terminal" , |
255 | )); |
256 | } |
257 | loop { |
258 | match self.read_key()? { |
259 | Key::Char(c) => { |
260 | return Ok(c); |
261 | } |
262 | Key::Enter => { |
263 | return Ok(' \n' ); |
264 | } |
265 | _ => {} |
266 | } |
267 | } |
268 | } |
269 | |
270 | /// Read a single key form the terminal. |
271 | /// |
272 | /// This does not echo anything. If the terminal is not user attended |
273 | /// the return value will always be the unknown key. |
274 | pub fn read_key(&self) -> io::Result<Key> { |
275 | if !self.is_tty { |
276 | Ok(Key::Unknown) |
277 | } else { |
278 | read_single_key() |
279 | } |
280 | } |
281 | |
282 | /// Read one line of input. |
283 | /// |
284 | /// This does not include the trailing newline. If the terminal is not |
285 | /// user attended the return value will always be an empty string. |
286 | pub fn read_line(&self) -> io::Result<String> { |
287 | if !self.is_tty { |
288 | return Ok("" .into()); |
289 | } |
290 | let mut rv = String::new(); |
291 | io::stdin().read_line(&mut rv)?; |
292 | let len = rv.trim_end_matches(&[' \r' , ' \n' ][..]).len(); |
293 | rv.truncate(len); |
294 | Ok(rv) |
295 | } |
296 | |
297 | /// Read one line of input with initial text. |
298 | /// |
299 | /// This does not include the trailing newline. If the terminal is not |
300 | /// user attended the return value will always be an empty string. |
301 | pub fn read_line_initial_text(&self, initial: &str) -> io::Result<String> { |
302 | if !self.is_tty { |
303 | return Ok("" .into()); |
304 | } |
305 | self.write_str(initial)?; |
306 | |
307 | let mut chars: Vec<char> = initial.chars().collect(); |
308 | |
309 | loop { |
310 | match self.read_key()? { |
311 | Key::Backspace => { |
312 | if chars.pop().is_some() { |
313 | self.clear_chars(1)?; |
314 | } |
315 | self.flush()?; |
316 | } |
317 | Key::Char(chr) => { |
318 | chars.push(chr); |
319 | let mut bytes_char = [0; 4]; |
320 | chr.encode_utf8(&mut bytes_char); |
321 | self.write_str(chr.encode_utf8(&mut bytes_char))?; |
322 | self.flush()?; |
323 | } |
324 | Key::Enter => { |
325 | self.write_line("" )?; |
326 | break; |
327 | } |
328 | _ => (), |
329 | } |
330 | } |
331 | Ok(chars.iter().collect::<String>()) |
332 | } |
333 | |
334 | /// Read a line of input securely. |
335 | /// |
336 | /// This is similar to `read_line` but will not echo the output. This |
337 | /// also switches the terminal into a different mode where not all |
338 | /// characters might be accepted. |
339 | pub fn read_secure_line(&self) -> io::Result<String> { |
340 | if !self.is_tty { |
341 | return Ok("" .into()); |
342 | } |
343 | match read_secure() { |
344 | Ok(rv) => { |
345 | self.write_line("" )?; |
346 | Ok(rv) |
347 | } |
348 | Err(err) => Err(err), |
349 | } |
350 | } |
351 | |
352 | /// Flush internal buffers. |
353 | /// |
354 | /// This forces the contents of the internal buffer to be written to |
355 | /// the terminal. This is unnecessary for unbuffered terminals which |
356 | /// will automatically flush. |
357 | pub fn flush(&self) -> io::Result<()> { |
358 | if let Some(ref buffer) = self.inner.buffer { |
359 | let mut buffer = buffer.lock().unwrap(); |
360 | if !buffer.is_empty() { |
361 | self.write_through(&buffer[..])?; |
362 | buffer.clear(); |
363 | } |
364 | } |
365 | Ok(()) |
366 | } |
367 | |
368 | /// Check if the terminal is indeed a terminal. |
369 | #[inline ] |
370 | pub fn is_term(&self) -> bool { |
371 | self.is_tty |
372 | } |
373 | |
374 | /// Check for common terminal features. |
375 | #[inline ] |
376 | pub fn features(&self) -> TermFeatures<'_> { |
377 | TermFeatures(self) |
378 | } |
379 | |
380 | /// Return the terminal size in rows and columns or gets sensible defaults. |
381 | #[inline ] |
382 | pub fn size(&self) -> (u16, u16) { |
383 | self.size_checked().unwrap_or((24, DEFAULT_WIDTH)) |
384 | } |
385 | |
386 | /// Return the terminal size in rows and columns. |
387 | /// |
388 | /// If the size cannot be reliably determined `None` is returned. |
389 | #[inline ] |
390 | pub fn size_checked(&self) -> Option<(u16, u16)> { |
391 | terminal_size(self) |
392 | } |
393 | |
394 | /// Move the cursor to row `x` and column `y`. Values are 0-based. |
395 | #[inline ] |
396 | pub fn move_cursor_to(&self, x: usize, y: usize) -> io::Result<()> { |
397 | move_cursor_to(self, x, y) |
398 | } |
399 | |
400 | /// Move the cursor up by `n` lines, if possible. |
401 | /// |
402 | /// If there are less than `n` lines above the current cursor position, |
403 | /// the cursor is moved to the top line of the terminal (i.e., as far up as possible). |
404 | #[inline ] |
405 | pub fn move_cursor_up(&self, n: usize) -> io::Result<()> { |
406 | move_cursor_up(self, n) |
407 | } |
408 | |
409 | /// Move the cursor down by `n` lines, if possible. |
410 | /// |
411 | /// If there are less than `n` lines below the current cursor position, |
412 | /// the cursor is moved to the bottom line of the terminal (i.e., as far down as possible). |
413 | #[inline ] |
414 | pub fn move_cursor_down(&self, n: usize) -> io::Result<()> { |
415 | move_cursor_down(self, n) |
416 | } |
417 | |
418 | /// Move the cursor `n` characters to the left, if possible. |
419 | /// |
420 | /// If there are fewer than `n` characters to the left of the current cursor position, |
421 | /// the cursor is moved to the beginning of the line (i.e., as far to the left as possible). |
422 | #[inline ] |
423 | pub fn move_cursor_left(&self, n: usize) -> io::Result<()> { |
424 | move_cursor_left(self, n) |
425 | } |
426 | |
427 | /// Move the cursor `n` characters to the right. |
428 | /// |
429 | /// If there are fewer than `n` characters to the right of the current cursor position, |
430 | /// the cursor is moved to the end of the current line (i.e., as far to the right as possible). |
431 | #[inline ] |
432 | pub fn move_cursor_right(&self, n: usize) -> io::Result<()> { |
433 | move_cursor_right(self, n) |
434 | } |
435 | |
436 | /// Clear the current line. |
437 | /// |
438 | /// Position the cursor at the beginning of the current line. |
439 | #[inline ] |
440 | pub fn clear_line(&self) -> io::Result<()> { |
441 | clear_line(self) |
442 | } |
443 | |
444 | /// Clear the last `n` lines before the current line. |
445 | /// |
446 | /// Position the cursor at the beginning of the first line that was cleared. |
447 | pub fn clear_last_lines(&self, n: usize) -> io::Result<()> { |
448 | self.move_cursor_up(n)?; |
449 | for _ in 0..n { |
450 | self.clear_line()?; |
451 | self.move_cursor_down(1)?; |
452 | } |
453 | self.move_cursor_up(n)?; |
454 | Ok(()) |
455 | } |
456 | |
457 | /// Clear the entire screen. |
458 | /// |
459 | /// Move the cursor to the upper left corner of the screen. |
460 | #[inline ] |
461 | pub fn clear_screen(&self) -> io::Result<()> { |
462 | clear_screen(self) |
463 | } |
464 | |
465 | /// Clear everything from the current cursor position to the end of the screen. |
466 | /// The cursor stays in its position. |
467 | #[inline ] |
468 | pub fn clear_to_end_of_screen(&self) -> io::Result<()> { |
469 | clear_to_end_of_screen(self) |
470 | } |
471 | |
472 | /// Clear the last `n` characters of the current line. |
473 | #[inline ] |
474 | pub fn clear_chars(&self, n: usize) -> io::Result<()> { |
475 | clear_chars(self, n) |
476 | } |
477 | |
478 | /// Set the terminal title. |
479 | pub fn set_title<T: Display>(&self, title: T) { |
480 | if !self.is_tty { |
481 | return; |
482 | } |
483 | set_title(title); |
484 | } |
485 | |
486 | /// Make the cursor visible again. |
487 | #[inline ] |
488 | pub fn show_cursor(&self) -> io::Result<()> { |
489 | show_cursor(self) |
490 | } |
491 | |
492 | /// Hide the cursor. |
493 | #[inline ] |
494 | pub fn hide_cursor(&self) -> io::Result<()> { |
495 | hide_cursor(self) |
496 | } |
497 | |
498 | // helpers |
499 | |
500 | #[cfg (all(windows, feature = "windows-console-colors" ))] |
501 | fn write_through(&self, bytes: &[u8]) -> io::Result<()> { |
502 | if self.is_msys_tty || !self.is_tty { |
503 | self.write_through_common(bytes) |
504 | } else { |
505 | match self.inner.target { |
506 | TermTarget::Stdout => console_colors(self, Console::stdout()?, bytes), |
507 | TermTarget::Stderr => console_colors(self, Console::stderr()?, bytes), |
508 | } |
509 | } |
510 | } |
511 | |
512 | #[cfg (not(all(windows, feature = "windows-console-colors" )))] |
513 | fn write_through(&self, bytes: &[u8]) -> io::Result<()> { |
514 | self.write_through_common(bytes) |
515 | } |
516 | |
517 | pub(crate) fn write_through_common(&self, bytes: &[u8]) -> io::Result<()> { |
518 | match self.inner.target { |
519 | TermTarget::Stdout => { |
520 | io::stdout().write_all(bytes)?; |
521 | io::stdout().flush()?; |
522 | } |
523 | TermTarget::Stderr => { |
524 | io::stderr().write_all(bytes)?; |
525 | io::stderr().flush()?; |
526 | } |
527 | #[cfg (unix)] |
528 | TermTarget::ReadWritePair(ReadWritePair { ref write, .. }) => { |
529 | let mut write = write.lock().unwrap(); |
530 | write.write_all(bytes)?; |
531 | write.flush()?; |
532 | } |
533 | } |
534 | Ok(()) |
535 | } |
536 | } |
537 | |
538 | /// A fast way to check if the application has a user attended for stdout. |
539 | /// |
540 | /// This means that stdout is connected to a terminal instead of a |
541 | /// file or redirected by other means. This is a shortcut for |
542 | /// checking the `is_attended` feature on the stdout terminal. |
543 | #[inline ] |
544 | pub fn user_attended() -> bool { |
545 | Term::stdout().features().is_attended() |
546 | } |
547 | |
548 | /// A fast way to check if the application has a user attended for stderr. |
549 | /// |
550 | /// This means that stderr is connected to a terminal instead of a |
551 | /// file or redirected by other means. This is a shortcut for |
552 | /// checking the `is_attended` feature on the stderr terminal. |
553 | #[inline ] |
554 | pub fn user_attended_stderr() -> bool { |
555 | Term::stderr().features().is_attended() |
556 | } |
557 | |
558 | #[cfg (unix)] |
559 | impl AsRawFd for Term { |
560 | fn as_raw_fd(&self) -> RawFd { |
561 | match self.inner.target { |
562 | TermTarget::Stdout => libc::STDOUT_FILENO, |
563 | TermTarget::Stderr => libc::STDERR_FILENO, |
564 | TermTarget::ReadWritePair(ReadWritePair { ref write: &Arc>, .. }) => { |
565 | write.lock().unwrap().as_raw_fd() |
566 | } |
567 | } |
568 | } |
569 | } |
570 | |
571 | #[cfg (windows)] |
572 | impl AsRawHandle for Term { |
573 | fn as_raw_handle(&self) -> RawHandle { |
574 | use windows_sys::Win32::System::Console::{ |
575 | GetStdHandle, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE, |
576 | }; |
577 | |
578 | unsafe { |
579 | GetStdHandle(match self.inner.target { |
580 | TermTarget::Stdout => STD_OUTPUT_HANDLE, |
581 | TermTarget::Stderr => STD_ERROR_HANDLE, |
582 | }) as RawHandle |
583 | } |
584 | } |
585 | } |
586 | |
587 | impl Write for Term { |
588 | fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
589 | match self.inner.buffer { |
590 | Some(ref buffer: &Mutex>) => buffer.lock().unwrap().write_all(buf), |
591 | None => self.write_through(bytes:buf), |
592 | }?; |
593 | Ok(buf.len()) |
594 | } |
595 | |
596 | fn flush(&mut self) -> io::Result<()> { |
597 | Term::flush(self) |
598 | } |
599 | } |
600 | |
601 | impl<'a> Write for &'a Term { |
602 | fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
603 | match self.inner.buffer { |
604 | Some(ref buffer: &Mutex>) => buffer.lock().unwrap().write_all(buf), |
605 | None => self.write_through(bytes:buf), |
606 | }?; |
607 | Ok(buf.len()) |
608 | } |
609 | |
610 | fn flush(&mut self) -> io::Result<()> { |
611 | Term::flush(self) |
612 | } |
613 | } |
614 | |
615 | impl Read for Term { |
616 | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
617 | io::stdin().read(buf) |
618 | } |
619 | } |
620 | |
621 | impl<'a> Read for &'a Term { |
622 | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
623 | io::stdin().read(buf) |
624 | } |
625 | } |
626 | |
627 | #[cfg (unix)] |
628 | pub use crate::unix_term::*; |
629 | #[cfg (target_arch = "wasm32" )] |
630 | pub use crate::wasm_term::*; |
631 | #[cfg (windows)] |
632 | pub use crate::windows_term::*; |
633 | |