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