1#[cfg(target_arch = "wasm32")]
41use crate::crossterm_stub as crossterm;
42use std::cell::Cell;
43use std::fmt;
44use std::io::{self, Write};
45use std::panic::UnwindSafe;
46use std::process::Command;
47use std::sync::Once;
48use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
49use std::time::{Duration, Instant};
50
51#[cfg(target_arch = "wasm32")]
52use crate::crossterm_stub::{from_crossterm, from_crossterm_kitty};
53use crate::event::Event;
54#[cfg(not(target_arch = "wasm32"))]
55use crate::event::{from_crossterm, from_crossterm_kitty};
56
57#[cfg(all(unix, feature = "termion"))]
58mod with_termion;
59#[cfg(feature = "termwiz")]
60mod with_termwiz;
61
62const RAW: u8 = 1;
63const ALTERNATE: u8 = 2;
64const MOUSE: u8 = 4;
65const PASTE: u8 = 8;
66const KITTY: u8 = 16;
67const SYNC: u8 = 32;
70const PIXELS: u8 = 64;
72
73const PUSH_KITTY: &str = "\x1b[>3u";
83const PUSH_KITTY_ALL: &str = "\x1b[>15u";
88#[cfg(feature = "termwiz")]
89const PUSH_KITTY_KEYS: &str = "\x1b[>1u";
90const POP_KITTY: &str = "\x1b[<1u";
91
92static ANSWERS: AtomicU8 = AtomicU8::new(0);
96
97pub const BEGIN_SYNCHRONIZED_UPDATE: &str = "\x1b[?2026h";
100pub const END_SYNCHRONIZED_UPDATE: &str = "\x1b[?2026l";
103#[cfg(unix)]
108const ASK_SYNC: &str = "\x1b[?2026$p";
109pub const SYNC_OUTPUT_ENV: &str = "RICH_SYNC_OUTPUT";
112#[cfg(unix)]
115const ASK_PIXELS: &str = "\x1b[?1016$p";
116pub const PIXEL_MOUSE_ENV: &str = "RICH_PIXEL_MOUSE";
120
121fn pixels_allowed(value: Option<&str>) -> bool {
124 value.map(str::trim) != Some("0")
125}
126
127pub fn synchronized_update(frame: &str) -> String {
129 if frame.is_empty() {
130 return String::new();
131 }
132 let mut out = String::with_capacity(
133 BEGIN_SYNCHRONIZED_UPDATE.len() + frame.len() + END_SYNCHRONIZED_UPDATE.len(),
134 );
135 out.push_str(BEGIN_SYNCHRONIZED_UPDATE);
136 out.push_str(frame);
137 out.push_str(END_SYNCHRONIZED_UPDATE);
138 out
139}
140
141fn sync_override(value: Option<&str>) -> Option<bool> {
144 match value.map(str::trim) {
145 Some("0") => Some(false),
146 Some("1") => Some(true),
147 _ => None,
148 }
149}
150
151fn sync_from_env() -> Option<bool> {
153 sync_override(std::env::var(SYNC_OUTPUT_ENV).ok().as_deref())
154}
155
156static ACTIVE: AtomicU8 = AtomicU8::new(0);
158static HOOK: Once = Once::new();
159static OWNED: AtomicBool = AtomicBool::new(false);
161static ON_STDERR: AtomicBool = AtomicBool::new(false);
163static LIBRARY: AtomicU8 = AtomicU8::new(0);
166
167#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
176#[non_exhaustive]
177pub enum BackendKind {
178 #[default]
182 Crossterm,
183 #[cfg(all(unix, feature = "termion"))]
188 Termion,
189 #[cfg(feature = "termwiz")]
195 Termwiz,
196}
197
198impl BackendKind {
199 pub const ALL: &'static [BackendKind] = &[
201 BackendKind::Crossterm,
202 #[cfg(all(unix, feature = "termion"))]
203 BackendKind::Termion,
204 #[cfg(feature = "termwiz")]
205 BackendKind::Termwiz,
206 ];
207
208 pub fn name(self) -> &'static str {
210 match self {
211 BackendKind::Crossterm => "crossterm",
212 #[cfg(all(unix, feature = "termion"))]
213 BackendKind::Termion => "termion",
214 #[cfg(feature = "termwiz")]
215 BackendKind::Termwiz => "termwiz",
216 }
217 }
218
219 pub fn from_name(name: &str) -> Option<BackendKind> {
221 BackendKind::ALL
222 .iter()
223 .copied()
224 .find(|kind| kind.name().eq_ignore_ascii_case(name))
225 }
226
227 fn index(self) -> u8 {
228 match self {
229 BackendKind::Crossterm => 0,
230 #[cfg(all(unix, feature = "termion"))]
231 BackendKind::Termion => 1,
232 #[cfg(feature = "termwiz")]
233 BackendKind::Termwiz => 2,
234 }
235 }
236
237 fn live() -> BackendKind {
239 let index = LIBRARY.load(Ordering::SeqCst);
240 BackendKind::ALL
241 .iter()
242 .copied()
243 .find(|kind| kind.index() == index)
244 .unwrap_or_default()
245 }
246
247 fn raw_mode(self, on: bool, wait: bool) -> io::Result<()> {
250 let _ = wait;
251 match self {
252 BackendKind::Crossterm if on => crossterm::terminal::enable_raw_mode(),
253 BackendKind::Crossterm => crossterm::terminal::disable_raw_mode(),
254 #[cfg(all(unix, feature = "termion"))]
255 BackendKind::Termion => with_termion::raw_mode(on, wait),
256 #[cfg(feature = "termwiz")]
257 BackendKind::Termwiz => with_termwiz::raw_mode(on, wait),
258 }
259 }
260
261 fn owns_alternate_screen(self) -> bool {
265 #[cfg(feature = "termwiz")]
266 if self == BackendKind::Termwiz {
267 return true;
268 }
269 false
270 }
271
272 fn alternate_screen(self, on: bool, wait: bool) -> Option<io::Result<()>> {
277 let _ = (on, wait);
278 match self {
279 #[cfg(feature = "termwiz")]
280 BackendKind::Termwiz => Some(with_termwiz::alternate_screen(on, wait)),
281 _ => None,
282 }
283 }
284
285 fn alternate_sequence(self, on: bool) -> String {
289 match self {
290 #[cfg(all(unix, feature = "termion"))]
291 BackendKind::Termion if on => termion::screen::ToAlternateScreen.to_string(),
292 #[cfg(all(unix, feature = "termion"))]
293 BackendKind::Termion => termion::screen::ToMainScreen.to_string(),
294 _ if on => "\x1b[?1049h".into(),
295 _ => "\x1b[?1049l".into(),
296 }
297 }
298
299 fn kitty_push(self) -> &'static str {
301 match self {
302 #[cfg(feature = "termwiz")]
303 BackendKind::Termwiz => PUSH_KITTY_KEYS,
304 _ => PUSH_KITTY,
305 }
306 }
307
308 fn after_raw_mode(self) -> &'static str {
314 match self {
315 #[cfg(all(unix, feature = "termwiz"))]
316 BackendKind::Termwiz => "\x1b[>4;0m",
317 _ => "",
318 }
319 }
320
321 fn release(self) {
323 match self {
324 #[cfg(all(unix, feature = "termion"))]
325 BackendKind::Termion => with_termion::release(),
326 #[cfg(feature = "termwiz")]
327 BackendKind::Termwiz => with_termwiz::release(),
328 _ => {}
329 }
330 }
331}
332
333impl fmt::Display for BackendKind {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 f.write_str(self.name())
336 }
337}
338
339#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
341pub enum Output {
342 #[default]
344 Stdout,
345 Stderr,
349}
350
351impl Output {
352 pub fn is_terminal(self) -> bool {
354 use std::io::IsTerminal;
355 match self {
356 Output::Stdout => io::stdout().is_terminal(),
357 Output::Stderr => io::stderr().is_terminal(),
358 }
359 }
360
361 fn write(self, text: &str) -> io::Result<()> {
362 match self {
363 Output::Stdout => {
364 let mut out = io::stdout().lock();
365 out.write_all(text.as_bytes())?;
366 out.flush()
367 }
368 Output::Stderr => {
369 let mut out = io::stderr().lock();
370 out.write_all(text.as_bytes())?;
371 out.flush()
372 }
373 }
374 }
375
376 fn live() -> Output {
378 if ON_STDERR.load(Ordering::SeqCst) {
379 Output::Stderr
380 } else {
381 Output::Stdout
382 }
383 }
384}
385
386pub(crate) fn keys_available() -> bool {
389 use std::io::IsTerminal;
390 if io::stdin().is_terminal() {
391 return true;
392 }
393 #[cfg(unix)]
394 let terminal = "/dev/tty";
395 #[cfg(windows)]
396 let terminal = "CONIN$";
397 #[cfg(any(unix, windows))]
398 return std::fs::OpenOptions::new()
399 .read(true)
400 .write(true)
401 .open(terminal)
402 .is_ok();
403 #[cfg(not(any(unix, windows)))]
404 false
405}
406
407fn restore(wait: bool) -> io::Result<()> {
410 let active = ACTIVE.swap(0, Ordering::SeqCst);
411 if active == 0 {
412 return Ok(());
413 }
414 let library = BackendKind::live();
415 let out = undo(active, library);
416 let written = Output::live().write(&out);
419 let left = if active & ALTERNATE != 0 {
420 library.alternate_screen(false, wait).unwrap_or(Ok(()))
421 } else {
422 Ok(())
423 };
424 let raw = if active & RAW != 0 {
425 let off = library.raw_mode(false, wait);
426 let after = match library.after_raw_mode() {
427 "" => Ok(()),
428 after => Output::live().write(after),
429 };
430 off.and(after)
431 } else {
432 Ok(())
433 };
434 written.and(left).and(raw)
435}
436
437fn undo(active: u8, library: BackendKind) -> String {
442 let mut out = String::new();
443 if active & SYNC != 0 {
444 out.push_str(END_SYNCHRONIZED_UPDATE);
445 }
446 if active & KITTY != 0 {
447 out.push_str(POP_KITTY);
448 }
449 if active & PIXELS != 0 {
450 out.push_str("\x1b[?1016l");
451 }
452 if active & MOUSE != 0 {
453 out.push_str("\x1b[?1006l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l");
454 }
455 if active & PASTE != 0 {
456 out.push_str("\x1b[?2004l");
457 }
458 out.push_str("\x1b[?25h");
459 if active & ALTERNATE != 0 && !library.owns_alternate_screen() {
460 out.push_str(&library.alternate_sequence(false));
461 }
462 out
463}
464
465static RAW_LINE: AtomicBool = AtomicBool::new(false);
469#[cfg(unix)]
470static SIGNALS: Once = Once::new();
471
472pub(crate) fn raw_line(on: bool) {
474 if on {
475 watch_signals();
476 }
477 RAW_LINE.store(on, Ordering::SeqCst);
478}
479
480fn watch_signals() {
488 #[cfg(unix)]
489 SIGNALS.call_once(|| {
490 use signal_hook::consts::{SIGHUP, SIGQUIT, SIGTERM, SIGTSTP};
491 let Ok(mut signals) =
492 signal_hook::iterator::Signals::new([SIGTERM, SIGHUP, SIGQUIT, SIGTSTP])
493 else {
494 return;
495 };
496 let _ = std::thread::Builder::new()
497 .name("rich-interact-signals".into())
498 .spawn(move || {
499 for signal in signals.forever() {
500 if signal == SIGTSTP {
501 suspend();
502 RESUMED.store(true, Ordering::SeqCst);
503 } else {
504 restore_for_signal();
505 run_teardowns();
506 let _ = signal_hook::low_level::emulate_default_handler(signal);
507 }
508 }
509 });
510 });
511}
512
513type Teardown = Box<dyn Fn() + Send + Sync>;
515static TEARDOWNS: std::sync::Mutex<Vec<Teardown>> = std::sync::Mutex::new(Vec::new());
516
517pub fn on_terminate(teardown: impl Fn() + Send + Sync + 'static) {
526 TEARDOWNS
527 .lock()
528 .unwrap_or_else(|e| e.into_inner())
529 .push(Box::new(teardown));
530}
531
532#[cfg(unix)]
534fn run_teardowns() {
535 let all = std::mem::take(&mut *TEARDOWNS.lock().unwrap_or_else(|e| e.into_inner()));
536 for teardown in all {
537 teardown();
538 }
539}
540
541#[cfg(unix)]
545fn restore_for_signal() {
546 let active = ACTIVE.swap(0, Ordering::SeqCst);
547 let library = BackendKind::live();
548 if active != 0 {
549 write_direct(&undo(active, library));
550 }
551 if active & ALTERNATE != 0 {
552 let _ = library.alternate_screen(false, true);
553 }
554 if active & RAW != 0 {
555 let _ = library.raw_mode(false, true);
556 write_direct(library.after_raw_mode());
557 }
558 if RAW_LINE.swap(false, Ordering::SeqCst) {
559 let _ = crossterm::terminal::disable_raw_mode();
560 }
561}
562
563static RESUMED: AtomicBool = AtomicBool::new(false);
566
567static SUSPENDING: std::sync::Mutex<()> = std::sync::Mutex::new(());
578
579#[cfg(unix)]
580fn suspend() {
581 let _suspending = SUSPENDING.lock().unwrap_or_else(|e| e.into_inner());
582 let active = ACTIVE.load(Ordering::SeqCst);
583 let raw_line = RAW_LINE.load(Ordering::SeqCst);
584 let library = BackendKind::live();
585 restore_for_signal();
586 let _ = signal_hook::low_level::emulate_default_handler(signal_hook::consts::SIGTSTP);
588 if active & RAW != 0 {
589 let _ = library.raw_mode(true, true);
590 }
591 if raw_line {
592 let _ = crossterm::terminal::enable_raw_mode();
593 }
594 RAW_LINE.store(raw_line, Ordering::SeqCst);
595 let mut out = String::new();
596 if active & ALTERNATE != 0 {
597 if library.alternate_screen(true, true).is_none() {
598 out.push_str(&library.alternate_sequence(true));
599 }
600 out.push_str("\x1b[H");
601 }
602 if active & KITTY != 0 {
603 out.push_str(library.kitty_push());
604 }
605 if active & MOUSE != 0 {
606 out.push_str("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
607 }
608 if active & PIXELS != 0 {
609 out.push_str("\x1b[?1016h");
610 }
611 if active & PASTE != 0 {
612 out.push_str("\x1b[?2004h");
613 }
614 if active != 0 {
615 out.push_str("\x1b[?25l");
616 }
617 ACTIVE.fetch_or(active, Ordering::SeqCst);
618 write_direct(&out);
619}
620
621#[cfg(unix)]
623fn write_direct(text: &str) {
624 if text.is_empty() {
625 return;
626 }
627 let direct = std::fs::OpenOptions::new()
628 .write(true)
629 .open("/dev/tty")
630 .and_then(|mut tty| tty.write_all(text.as_bytes()));
631 if direct.is_err() {
632 let _ = Output::live().write(text);
633 }
634}
635
636#[cfg(any(all(unix, feature = "termion"), feature = "termwiz"))]
640fn lock<T>(mutex: &std::sync::Mutex<T>, wait: bool) -> io::Result<std::sync::MutexGuard<'_, T>> {
641 use std::sync::TryLockError;
642 if wait {
643 return Ok(mutex.lock().unwrap_or_else(|e| e.into_inner()));
644 }
645 match mutex.try_lock() {
646 Ok(guard) => Ok(guard),
647 Err(TryLockError::Poisoned(e)) => Ok(e.into_inner()),
648 Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()),
649 }
650}
651
652#[cfg(unix)]
656const ANSWER_WAIT: Duration = Duration::from_secs(1);
657
658#[cfg(unix)]
662const ANSWER_WAIT_TYPING: Duration = Duration::from_millis(250);
663
664#[cfg(unix)]
667struct Asked {
668 bytes: Vec<u8>,
669 answered: bool,
670}
671
672#[cfg(unix)]
678fn ask_terminal(query: &str, done: impl Fn(&[u8]) -> bool) -> Option<Asked> {
679 use std::io::Read;
680 use std::os::unix::io::AsRawFd;
681 let mut tty = std::fs::OpenOptions::new()
682 .read(true)
683 .write(true)
684 .open("/dev/tty")
685 .ok()?;
686 tty.write_all(query.as_bytes()).ok()?;
687 tty.flush().ok()?;
688 let mut end = Instant::now() + ANSWER_WAIT;
689 let mut typing = false;
690 let mut bytes = Vec::new();
691 loop {
692 if done(&bytes) {
693 return Some(Asked {
694 bytes,
695 answered: true,
696 });
697 }
698 if !typing && !strip_answers(&bytes).is_empty() {
699 typing = true;
700 end = end.min(Instant::now() + ANSWER_WAIT_TYPING);
701 }
702 let left = end.saturating_duration_since(Instant::now());
703 if left.is_zero() {
704 break;
705 }
706 let mut ready = [filedescriptor::pollfd {
707 fd: tty.as_raw_fd(),
708 events: filedescriptor::POLLIN,
709 revents: 0,
710 }];
711 match filedescriptor::poll(&mut ready, Some(left)) {
712 Ok(0) => break,
713 Ok(_) => {}
714 Err(filedescriptor::Error::Poll(e)) if e.kind() == io::ErrorKind::Interrupted => {
715 continue;
716 }
717 Err(_) => break,
718 }
719 let mut buffer = [0u8; 256];
720 match tty.read(&mut buffer) {
721 Ok(0) | Err(_) => break,
722 Ok(read) => bytes.extend_from_slice(&buffer[..read]),
723 }
724 }
725 Some(Asked {
726 bytes,
727 answered: false,
728 })
729}
730
731#[cfg(any(unix, test))]
734fn strip_answers(bytes: &[u8]) -> Vec<u8> {
735 let mut rest = Vec::new();
736 let mut at = 0;
737 while at < bytes.len() {
738 if !bytes[at..].starts_with(b"\x1b[?") {
739 rest.push(bytes[at]);
740 at += 1;
741 continue;
742 }
743 let mut end = at + 3;
744 while end < bytes.len() && (0x20..=0x3f).contains(&bytes[end]) {
745 end += 1;
746 }
747 match bytes.get(end) {
748 None => break,
750 Some(0x40..=0x7e) => at = end + 1,
751 Some(_) => at = end,
752 }
753 }
754 rest
755}
756
757#[cfg(any(unix, test))]
761#[derive(Clone, Copy, Debug, PartialEq, Eq)]
762struct PrivateAnswer<'a> {
763 parameters: &'a [u8],
764 intermediates: &'a [u8],
765 last: u8,
766}
767
768#[cfg(any(unix, test))]
771fn private_answers(bytes: &[u8]) -> impl Iterator<Item = PrivateAnswer<'_>> + '_ {
772 let mut at = 0;
773 std::iter::from_fn(move || {
774 while at + 3 <= bytes.len() {
775 if !bytes[at..].starts_with(b"\x1b[?") {
776 at += 1;
777 continue;
778 }
779 let start = at + 3;
780 let mut end = start;
781 while end < bytes.len() && (0x30..=0x3f).contains(&bytes[end]) {
782 end += 1;
783 }
784 let middle = end;
785 while end < bytes.len() && (0x20..=0x2f).contains(&bytes[end]) {
786 end += 1;
787 }
788 match bytes.get(end) {
789 None => return None,
790 Some(&last @ 0x40..=0x7e) => {
791 at = end + 1;
792 return Some(PrivateAnswer {
793 parameters: &bytes[start..middle],
794 intermediates: &bytes[middle..end],
795 last,
796 });
797 }
798 Some(_) => at = end,
800 }
801 }
802 None
803 })
804}
805
806#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
808struct Answers {
809 kitty: bool,
811 sync: bool,
813 pixels: bool,
815}
816
817impl Answers {
818 fn encode(self) -> u8 {
820 1 | (u8::from(self.kitty) << 1) | (u8::from(self.sync) << 2) | (u8::from(self.pixels) << 3)
821 }
822
823 fn decode(bits: u8) -> Answers {
824 Answers {
825 kitty: bits & 2 != 0,
826 sync: bits & 4 != 0,
827 pixels: bits & 8 != 0,
828 }
829 }
830
831 #[cfg(any(unix, test))]
837 fn read(bytes: &[u8]) -> Option<Answers> {
838 let mut answers = Answers::default();
839 for answer in private_answers(bytes) {
840 match (answer.last, answer.intermediates) {
841 (b'c', b"") => return Some(answers),
842 (b'u', b"") => answers.kitty = true,
843 (b'y', b"$") => {
844 if let Some(state) = answer.parameters.strip_prefix(b"2026;") {
845 answers.sync = matches!(state, b"1" | b"2" | b"3");
846 }
847 if let Some(state) = answer.parameters.strip_prefix(b"1016;") {
848 answers.pixels = matches!(state, b"1" | b"2" | b"3");
849 }
850 }
851 _ => {}
852 }
853 }
854 None
855 }
856}
857
858#[cfg(unix)]
864fn ask_start() -> (Answers, Vec<u8>) {
865 let query = format!("{ASK_SYNC}{ASK_PIXELS}\x1b[?u\x1b[c");
866 match ask_terminal(&query, |bytes| Answers::read(bytes).is_some()) {
867 Some(asked) => {
868 let answers = match asked.answered {
869 true => Answers::read(&asked.bytes).unwrap_or_default(),
870 false => Answers::default(),
871 };
872 (answers, strip_answers(&asked.bytes))
873 }
874 None => (Answers::default(), Vec::new()),
875 }
876}
877
878#[cfg(any(all(unix, any(feature = "termion", feature = "termwiz")), test))]
882fn cursor_report(bytes: &[u8]) -> Option<(u16, std::ops::Range<usize>)> {
883 let digits = |from: usize| {
884 from + bytes[from..]
885 .iter()
886 .take_while(|b| b.is_ascii_digit())
887 .count()
888 };
889 let mut at = 0;
890 while let Some(found) = bytes[at..].windows(2).position(|w| w == b"\x1b[") {
891 let start = at + found;
892 let row_end = digits(start + 2);
893 if row_end > start + 2 && bytes.get(row_end) == Some(&b';') {
894 let end = digits(row_end + 1);
895 if end > row_end + 1 && bytes.get(end) == Some(&b'R') {
896 let row = std::str::from_utf8(&bytes[start + 2..row_end]).map(str::parse::<u16>);
897 if let Ok(Ok(row)) = row {
898 return Some((row.saturating_sub(1), start..end + 1));
899 }
900 }
901 }
902 at = start + 1;
903 }
904 None
905}
906
907#[cfg(all(unix, any(feature = "termion", feature = "termwiz")))]
910fn ask_cursor_row() -> (Option<u16>, Vec<u8>) {
911 let Some(Asked { bytes, .. }) = ask_terminal("\x1b[6n", |bytes| cursor_report(bytes).is_some())
912 else {
913 return (None, Vec::new());
914 };
915 match cursor_report(&bytes) {
916 Some((row, at)) => {
917 let mut rest = bytes[..at.start].to_vec();
918 rest.extend_from_slice(&bytes[at.end..]);
919 (Some(row), rest)
920 }
921 None => (None, bytes),
922 }
923}
924
925fn cell_size() -> Option<(u16, u16)> {
929 let size = crossterm::terminal::window_size().ok()?;
930 cell_of(size.columns, size.rows, size.width, size.height)
931}
932
933fn cell_of(columns: u16, rows: u16, width: u16, height: u16) -> Option<(u16, u16)> {
936 if columns == 0 || rows == 0 || width < columns || height < rows {
937 return None;
938 }
939 Some((width / columns, height / rows))
940}
941
942thread_local! {
943 static CATCHING: Cell<usize> = const { Cell::new(0) };
945}
946
947pub fn catch_panic<R>(f: impl FnOnce() -> R + UnwindSafe) -> std::thread::Result<R> {
954 CATCHING.with(|depth| depth.set(depth.get() + 1));
955 let result = std::panic::catch_unwind(f);
956 CATCHING.with(|depth| depth.set(depth.get() - 1));
957 result
958}
959
960fn install_hook() {
961 HOOK.call_once(|| {
962 let previous = std::panic::take_hook();
963 std::panic::set_hook(Box::new(move |info| {
964 if CATCHING.with(Cell::get) > 0 && ACTIVE.load(Ordering::SeqCst) != 0 {
967 return;
968 }
969 let _ = restore(false);
970 previous(info);
971 }));
972 });
973}
974
975#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
977pub struct SessionOptions {
978 pub alternate_screen: bool,
981 pub mouse: bool,
983 pub bracketed_paste: bool,
985 pub output: Output,
987 pub legacy_keys: bool,
993 pub backend: BackendKind,
996 pub synchronized_output: Option<bool>,
1004 pub all_keys: bool,
1013}
1014
1015#[derive(Clone)]
1038pub struct Waker(std::sync::Arc<WakeState>);
1039
1040struct WakeState {
1041 woken: std::sync::Mutex<bool>,
1042 changed: std::sync::Condvar,
1043 #[cfg(unix)]
1046 pipe: Option<(
1047 std::os::unix::net::UnixStream,
1048 std::os::unix::net::UnixStream,
1049 )>,
1050}
1051
1052impl Default for Waker {
1053 fn default() -> Waker {
1054 Waker::new()
1055 }
1056}
1057
1058impl fmt::Debug for Waker {
1059 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1060 f.debug_struct("Waker")
1061 .field("woken", &*self.lock())
1062 .finish()
1063 }
1064}
1065
1066impl Waker {
1067 pub fn new() -> Waker {
1068 #[cfg(unix)]
1069 let pipe = std::os::unix::net::UnixStream::pair()
1070 .ok()
1071 .filter(|(a, b)| a.set_nonblocking(true).is_ok() && b.set_nonblocking(true).is_ok());
1072 Waker(std::sync::Arc::new(WakeState {
1073 woken: std::sync::Mutex::new(false),
1074 changed: std::sync::Condvar::new(),
1075 #[cfg(unix)]
1076 pipe,
1077 }))
1078 }
1079
1080 fn lock(&self) -> std::sync::MutexGuard<'_, bool> {
1081 self.0.woken.lock().unwrap_or_else(|e| e.into_inner())
1082 }
1083
1084 pub fn wake(&self) {
1086 let mut woken = self.lock();
1087 if std::mem::replace(&mut *woken, true) {
1088 return;
1090 }
1091 #[cfg(unix)]
1092 if let Some((_, writer)) = &self.0.pipe {
1093 use std::io::Write as _;
1094 let _ = (&*writer).write(&[1]);
1095 }
1096 self.0.changed.notify_all();
1097 }
1098
1099 pub fn take(&self) -> bool {
1102 let mut woken = self.lock();
1103 #[cfg(unix)]
1104 if let Some((reader, _)) = &self.0.pipe {
1105 use std::io::Read as _;
1106 let mut drained = [0u8; 16];
1107 while matches!((&*reader).read(&mut drained), Ok(n) if n > 0) {}
1108 }
1109 std::mem::replace(&mut *woken, false)
1110 }
1111
1112 pub fn wait(&self, timeout: Duration) -> bool {
1115 {
1116 let woken = self.lock();
1117 let _ = self
1118 .0
1119 .changed
1120 .wait_timeout_while(woken, timeout, |woken| !*woken)
1121 .unwrap_or_else(|e| e.into_inner());
1122 }
1123 self.take()
1124 }
1125
1126 #[cfg(unix)]
1129 pub(crate) fn fd(&self) -> Option<std::os::unix::io::RawFd> {
1130 use std::os::unix::io::AsRawFd;
1131 self.0.pipe.as_ref().map(|(reader, _)| reader.as_raw_fd())
1132 }
1133}
1134
1135const WAKE_SLICE: Duration = Duration::from_millis(4);
1138
1139const KEYS_SLICE: Duration = Duration::from_millis(20);
1143
1144#[cfg(unix)]
1146pub(crate) type WakeFd = std::os::unix::io::RawFd;
1147#[cfg(not(unix))]
1148pub(crate) type WakeFd = ();
1149
1150#[cfg(unix)]
1153fn input_before_wake(
1154 tty: std::os::unix::io::RawFd,
1155 wake: std::os::unix::io::RawFd,
1156 wait: Duration,
1157) -> io::Result<bool> {
1158 use filedescriptor::{POLLIN, poll, pollfd};
1159 let mut ready = [
1160 pollfd {
1161 fd: tty,
1162 events: POLLIN,
1163 revents: 0,
1164 },
1165 pollfd {
1166 fd: wake,
1167 events: POLLIN,
1168 revents: 0,
1169 },
1170 ];
1171 match poll(&mut ready, Some(wait)) {
1172 Ok(_) => Ok(ready[0].revents != 0 && ready[1].revents == 0),
1173 Err(filedescriptor::Error::Poll(e)) if e.kind() == io::ErrorKind::Interrupted => Ok(false),
1174 Err(e) => Err(io::Error::other(e.to_string())),
1175 }
1176}
1177
1178pub trait Backend {
1181 fn size(&self) -> (u16, u16);
1183 fn read(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>>;
1186 fn write(&mut self, text: &str) -> io::Result<()>;
1188 fn synchronized_output(&self) -> bool {
1191 false
1192 }
1193 fn write_frame(&mut self, frame: &str) -> io::Result<()> {
1197 if frame.is_empty() {
1198 Ok(())
1199 } else if self.synchronized_output() {
1200 self.write(&synchronized_update(frame))
1201 } else {
1202 self.write(frame)
1203 }
1204 }
1205 fn elapsed(&self) -> Duration;
1207 fn handoff(&mut self, command: &mut Command) -> io::Result<Option<i32>>;
1210 fn alternate_screen(&self) -> bool;
1212 fn origin(&self) -> u16 {
1215 0
1216 }
1217 fn painted(&mut self, text: &str) {
1220 let _ = text;
1221 }
1222 fn can_suspend(&self) -> bool {
1225 false
1226 }
1227 fn suspend(&mut self) -> io::Result<()> {
1231 Ok(())
1232 }
1233 fn take_resumed(&mut self) -> bool {
1236 false
1237 }
1238 fn clipboard(&self) -> Result<(), String> {
1241 Err("this backend has no clipboard".into())
1242 }
1243 fn copy(&mut self, text: &str) -> io::Result<()> {
1246 let _ = text;
1247 Ok(())
1248 }
1249 fn waker(&mut self) -> Option<Waker> {
1254 None
1255 }
1256 fn releases_reported(&self) -> crate::Releases {
1259 crate::Releases::None
1260 }
1261}
1262
1263enum Input {
1266 Crossterm,
1267 #[cfg(all(unix, feature = "termion"))]
1268 Termion(with_termion::Reader),
1269 #[cfg(feature = "termwiz")]
1270 Termwiz {
1271 held: crate::event::HeldButton,
1272 probed: with_termwiz::Probed,
1273 },
1274}
1275
1276impl Input {
1277 fn open(kind: BackendKind, output: Output) -> io::Result<Input> {
1278 let _ = output;
1279 Ok(match kind {
1280 BackendKind::Crossterm => Input::Crossterm,
1281 #[cfg(all(unix, feature = "termion"))]
1282 BackendKind::Termion => Input::Termion(with_termion::Reader::open()?),
1283 #[cfg(feature = "termwiz")]
1284 BackendKind::Termwiz => Input::Termwiz {
1285 held: Default::default(),
1286 probed: with_termwiz::open(output)?,
1287 },
1288 })
1289 }
1290}
1291
1292pub struct Session {
1296 options: SessionOptions,
1297 start: Instant,
1298 active: bool,
1299 origin: u16,
1302 clipboard: rich_ext::clipboard::Clipboard,
1304 kitty: bool,
1306 sync: bool,
1308 cell: Option<(u16, u16)>,
1311 input: Option<Input>,
1313 waker: Option<Waker>,
1315 typed: std::collections::VecDeque<Event>,
1318 looked: Instant,
1321 #[cfg(unix)]
1324 tty: Option<std::fs::File>,
1325}
1326
1327fn answered(ask: impl FnOnce() -> (Answers, Vec<u8>)) -> (Answers, Vec<u8>) {
1333 match ANSWERS.load(Ordering::SeqCst) {
1334 0 => {
1335 let (answers, typed) = if query_reaches_terminal() {
1336 ask()
1337 } else {
1338 (Answers::default(), Vec::new())
1339 };
1340 ANSWERS.store(answers.encode(), Ordering::SeqCst);
1341 (answers, typed)
1342 }
1343 bits => (Answers::decode(bits), Vec::new()),
1344 }
1345}
1346
1347fn query_reaches_terminal() -> bool {
1355 use std::io::IsTerminal;
1356 io::stdout().is_terminal()
1357}
1358
1359impl Session {
1360 pub fn start(options: SessionOptions) -> io::Result<Session> {
1363 if OWNED
1364 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1365 .is_err()
1366 {
1367 return Err(io::Error::new(
1368 io::ErrorKind::ResourceBusy,
1369 "a terminal session is already running",
1370 ));
1371 }
1372 install_hook();
1373 watch_signals();
1374 let mut session = Session {
1377 options,
1378 start: Instant::now(),
1379 active: false,
1380 origin: 0,
1381 clipboard: rich_ext::clipboard::Clipboard::detect(
1382 &crate::clipboard::SessionEnvironment,
1383 ),
1384 kitty: false,
1385 sync: false,
1386 cell: None,
1387 input: None,
1388 waker: None,
1389 typed: Default::default(),
1390 looked: Instant::now(),
1391 #[cfg(unix)]
1392 tty: None,
1393 };
1394 session.enter()?;
1395 Ok(session)
1396 }
1397
1398 fn enter(&mut self) -> io::Result<()> {
1399 let library = self.options.backend;
1400 ON_STDERR.store(self.options.output == Output::Stderr, Ordering::SeqCst);
1401 LIBRARY.store(library.index(), Ordering::SeqCst);
1402 if self.input.is_none() {
1403 self.input = Some(Input::open(library, self.options.output)?);
1404 }
1405 library.raw_mode(true, true)?;
1406 ACTIVE.fetch_or(RAW, Ordering::SeqCst);
1407 self.active = true;
1410 let mut out = String::new();
1411 if self.options.alternate_screen {
1412 ACTIVE.fetch_or(ALTERNATE, Ordering::SeqCst);
1413 match library.alternate_screen(true, true) {
1414 Some(entered) => entered?,
1415 None => out.push_str(&library.alternate_sequence(true)),
1416 }
1417 out.push_str("\x1b[H");
1418 }
1419 let answers = if self.options.legacy_keys {
1424 Answers::default()
1425 } else {
1426 self.answered()
1427 };
1428 self.kitty = answers.kitty;
1429 if self.kitty {
1430 out.push_str(match (library, self.options.all_keys) {
1431 (BackendKind::Crossterm, true) => PUSH_KITTY_ALL,
1432 _ => library.kitty_push(),
1433 });
1434 }
1435 self.sync = self
1436 .options
1437 .synchronized_output
1438 .or_else(sync_from_env)
1439 .unwrap_or(answers.sync);
1440 if self.sync {
1441 ACTIVE.fetch_or(SYNC, Ordering::SeqCst);
1442 }
1443 let (mouse, paste) = self.capable();
1444 if self.options.mouse && mouse {
1445 out.push_str("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
1446 ACTIVE.fetch_or(MOUSE, Ordering::SeqCst);
1447 self.cell = None;
1450 if answers.pixels
1451 && pixels_allowed(std::env::var(PIXEL_MOUSE_ENV).ok().as_deref())
1452 && let Some(cell) = cell_size()
1453 {
1454 out.push_str("\x1b[?1016h");
1455 ACTIVE.fetch_or(PIXELS, Ordering::SeqCst);
1456 self.cell = Some(cell);
1457 }
1458 }
1459 if self.options.bracketed_paste && paste {
1460 out.push_str("\x1b[?2004h");
1461 ACTIVE.fetch_or(PASTE, Ordering::SeqCst);
1462 }
1463 out.push_str("\x1b[?25l");
1464 {
1465 let _suspending = SUSPENDING.lock().unwrap_or_else(|e| e.into_inner());
1470 if self.kitty {
1471 ACTIVE.fetch_or(KITTY, Ordering::SeqCst);
1472 }
1473 self.options.output.write(&out)?;
1474 }
1475 if self.options.mouse
1479 && !self.options.alternate_screen
1480 && self.options.output == Output::Stdout
1481 {
1482 self.origin = self.cursor_row().unwrap_or(0);
1483 }
1484 Ok(())
1485 }
1486
1487 fn answered(&mut self) -> Answers {
1492 let (answers, typed) = match self.options.backend {
1493 #[cfg(unix)]
1494 BackendKind::Crossterm => answered(ask_start),
1495 #[cfg(not(unix))]
1497 BackendKind::Crossterm => answered(|| {
1498 let kitty = crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false);
1499 (
1500 Answers {
1501 kitty,
1502 ..Answers::default()
1503 },
1504 Vec::new(),
1505 )
1506 }),
1507 #[cfg(all(unix, feature = "termion"))]
1509 BackendKind::Termion => (Answers::default(), Vec::new()),
1510 #[cfg(all(unix, feature = "termwiz"))]
1511 BackendKind::Termwiz => answered(ask_start),
1512 #[cfg(all(not(unix), feature = "termwiz"))]
1513 BackendKind::Termwiz => (Answers::default(), Vec::new()),
1514 };
1515 #[cfg(unix)]
1516 if !typed.is_empty() {
1517 match &mut self.input {
1518 #[cfg(feature = "termwiz")]
1519 Some(Input::Termwiz { probed, .. }) => with_termwiz::unread(probed, &typed),
1520 _ => self.typed.extend(crate::event::from_legacy_bytes(&typed)),
1524 }
1525 }
1526 #[cfg(not(unix))]
1528 debug_assert!(typed.is_empty());
1529 answers
1530 }
1531
1532 fn capable(&self) -> (bool, bool) {
1535 match &self.input {
1536 #[cfg(feature = "termwiz")]
1537 Some(Input::Termwiz { probed, .. }) => (probed.mouse, probed.paste),
1538 _ => (true, true),
1539 }
1540 }
1541
1542 fn cursor_row(&mut self) -> Option<u16> {
1545 match self.options.backend {
1546 BackendKind::Crossterm => crossterm::cursor::position().ok().map(|(_, row)| row),
1547 #[cfg(all(unix, any(feature = "termion", feature = "termwiz")))]
1548 #[allow(unreachable_patterns)]
1549 _ => {
1550 let (row, rest) = ask_cursor_row();
1551 match &mut self.input {
1552 #[cfg(feature = "termion")]
1553 Some(Input::Termion(reader)) => reader.unread(&rest),
1554 #[cfg(feature = "termwiz")]
1555 Some(Input::Termwiz { probed, .. }) => with_termwiz::unread(probed, &rest),
1556 _ => {}
1557 }
1558 row
1559 }
1560 #[cfg(all(not(unix), feature = "termwiz"))]
1561 _ => None,
1562 }
1563 }
1564
1565 fn measure(&self) -> io::Result<(u16, u16)> {
1567 match &self.input {
1568 #[cfg(all(unix, feature = "termion"))]
1569 Some(Input::Termion(reader)) => reader.size(),
1570 #[cfg(feature = "termwiz")]
1571 Some(Input::Termwiz { .. }) => with_termwiz::size(),
1572 _ => crossterm::terminal::size(),
1573 }
1574 }
1575
1576 fn in_cells(&mut self, event: Event) -> Event {
1582 let Some(cell) = self.cell else {
1583 return event;
1584 };
1585 match event {
1586 Event::Mouse(mouse) => match mouse.reread_as_pixels(cell) {
1587 Some(read) => Event::Mouse(read),
1588 None => Event::Mouse(mouse),
1589 },
1590 Event::Resize { .. } => {
1591 if let Some(size) = cell_size() {
1592 self.cell = Some(size);
1593 }
1594 event
1595 }
1596 event => event,
1597 }
1598 }
1599
1600 fn next_event(&mut self, wait: Duration) -> io::Result<Option<Event>> {
1601 let kitty = self.kitty;
1602 #[cfg(unix)]
1603 let wake: Option<WakeFd> = self.waker.as_ref().and_then(Waker::fd);
1604 #[cfg(not(unix))]
1605 let wake: Option<WakeFd> = None;
1606 match &mut self.input {
1607 #[cfg(all(unix, feature = "termion"))]
1608 Some(Input::Termion(reader)) => reader.next(wait, wake),
1609 #[cfg(feature = "termwiz")]
1610 Some(Input::Termwiz { held, probed }) => Ok(with_termwiz::poll(probed, wait, wake)?
1611 .and_then(|event| {
1612 if kitty {
1613 crate::event::from_termwiz_kitty(event, held)
1614 } else {
1615 crate::event::from_termwiz(event, held)
1616 }
1617 })),
1618 _ => {
1619 #[cfg(unix)]
1624 if let Some(wake) = wake {
1625 if !crossterm::event::poll(Duration::from_millis(1))? {
1626 let tty = self.crossterm_tty()?;
1627 if !input_before_wake(tty, wake, wait)? {
1629 return Ok(None);
1630 }
1631 if !crossterm::event::poll(wait.min(Duration::from_millis(10)))? {
1634 return Ok(None);
1635 }
1636 }
1637 } else if !crossterm::event::poll(wait)? {
1638 return Ok(None);
1639 }
1640 #[cfg(not(unix))]
1641 {
1642 let _ = wake;
1643 if !crossterm::event::poll(wait)? {
1644 return Ok(None);
1645 }
1646 }
1647 let event = crossterm::event::read()?;
1648 Ok(if kitty {
1649 from_crossterm_kitty(event)
1650 } else {
1651 from_crossterm(event)
1652 })
1653 }
1654 }
1655 }
1656
1657 #[cfg(unix)]
1660 fn crossterm_tty(&mut self) -> io::Result<std::os::unix::io::RawFd> {
1661 use std::io::IsTerminal;
1662 use std::os::unix::io::AsRawFd;
1663 if io::stdin().is_terminal() {
1664 return Ok(io::stdin().as_raw_fd());
1665 }
1666 if self.tty.is_none() {
1667 self.tty = Some(std::fs::File::open("/dev/tty")?);
1668 }
1669 Ok(self.tty.as_ref().map_or(0, |tty| tty.as_raw_fd()))
1670 }
1671
1672 pub fn leave(&mut self) -> io::Result<()> {
1674 if !std::mem::take(&mut self.active) {
1675 return Ok(());
1676 }
1677 let _suspending = SUSPENDING.lock().unwrap_or_else(|e| e.into_inner());
1680 restore(true)
1681 }
1682
1683 pub fn options(&self) -> SessionOptions {
1684 self.options
1685 }
1686
1687 pub fn kitty_keys(&self) -> bool {
1692 self.kitty
1693 }
1694
1695 pub fn releases_reported(&self) -> crate::Releases {
1700 use crate::Releases;
1701 match self.options.backend {
1702 BackendKind::Crossterm if cfg!(windows) => Releases::All,
1703 BackendKind::Crossterm if self.kitty && self.options.all_keys => Releases::All,
1704 BackendKind::Crossterm if self.kitty => Releases::ExceptText,
1705 #[allow(unreachable_patterns)]
1706 _ => Releases::None,
1707 }
1708 }
1709
1710 pub fn synchronized_output(&self) -> bool {
1714 self.sync
1715 }
1716
1717 pub fn color_system(&self) -> Option<Option<rich::ColorSystem>> {
1721 match &self.input {
1722 #[cfg(feature = "termwiz")]
1723 Some(Input::Termwiz { probed, .. }) => Some(probed.color),
1724 _ => None,
1725 }
1726 }
1727
1728 fn repaint(&mut self) -> Event {
1731 if self.options.mouse
1732 && !self.options.alternate_screen
1733 && self.options.output == Output::Stdout
1734 {
1735 self.origin = self.cursor_row().unwrap_or(0);
1736 }
1737 let (columns, rows) = self.measure().unwrap_or((80, 24));
1738 Event::Resize { columns, rows }
1739 }
1740}
1741
1742impl Drop for Session {
1743 fn drop(&mut self) {
1744 let _ = self.leave();
1745 if self.input.take().is_some() {
1746 self.options.backend.release();
1747 }
1748 OWNED.store(false, Ordering::SeqCst);
1749 }
1750}
1751
1752impl Backend for Session {
1753 fn size(&self) -> (u16, u16) {
1754 self.measure().unwrap_or((80, 24))
1755 }
1756
1757 fn read(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>> {
1758 if let Some(event) = self.typed.pop_front() {
1759 return Ok(Some(event));
1760 }
1761 let end = timeout.map(|timeout| Instant::now() + timeout);
1762 loop {
1763 if RESUMED.load(Ordering::SeqCst) {
1766 return Ok(Some(self.repaint()));
1767 }
1768 if self.waker.as_ref().is_some_and(Waker::take) {
1774 if self.looked.elapsed() < KEYS_SLICE {
1775 return Ok(None);
1776 }
1777 self.looked = Instant::now();
1778 return Ok(self.next_event(Duration::ZERO)?.map(|e| self.in_cells(e)));
1779 }
1780 let mut wait = match end {
1783 Some(end) => end.saturating_duration_since(Instant::now()),
1784 None => Duration::from_secs(3600),
1785 }
1786 .min(Duration::from_millis(250));
1787 if self.waker.is_some() && !cfg!(unix) {
1788 wait = wait.min(WAKE_SLICE);
1789 }
1790 self.looked = Instant::now();
1793 if let Some(event) = self.next_event(wait)? {
1794 return Ok(Some(self.in_cells(event)));
1795 }
1796 if end.is_some_and(|end| Instant::now() >= end) {
1797 return Ok(None);
1798 }
1799 }
1800 }
1801
1802 fn write(&mut self, text: &str) -> io::Result<()> {
1803 self.options.output.write(text)
1804 }
1805
1806 fn waker(&mut self) -> Option<Waker> {
1810 Some(self.waker.get_or_insert_with(Waker::new).clone())
1811 }
1812
1813 fn releases_reported(&self) -> crate::Releases {
1814 Session::releases_reported(self)
1815 }
1816
1817 fn synchronized_output(&self) -> bool {
1818 self.sync
1819 }
1820
1821 fn clipboard(&self) -> Result<(), String> {
1822 let field = self.clipboard.field();
1823 if field.value {
1824 Ok(())
1825 } else {
1826 Err(field.reason.clone())
1827 }
1828 }
1829
1830 fn copy(&mut self, text: &str) -> io::Result<()> {
1831 let sequence = rich_ext::clipboard::osc52(text)
1832 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error.to_string()))?;
1833 self.options.output.write(&sequence)
1834 }
1835
1836 fn elapsed(&self) -> Duration {
1837 self.start.elapsed()
1838 }
1839
1840 fn handoff(&mut self, command: &mut Command) -> io::Result<Option<i32>> {
1841 self.leave()?;
1842 let status = command.status();
1843 self.enter()?;
1844 Ok(status?.code())
1845 }
1846
1847 fn alternate_screen(&self) -> bool {
1848 self.options.alternate_screen
1849 }
1850
1851 fn origin(&self) -> u16 {
1852 self.origin
1853 }
1854
1855 fn can_suspend(&self) -> bool {
1856 cfg!(unix)
1857 }
1858
1859 fn suspend(&mut self) -> io::Result<()> {
1860 #[cfg(unix)]
1861 {
1862 suspend();
1863 self.repaint();
1864 }
1865 Ok(())
1866 }
1867
1868 fn take_resumed(&mut self) -> bool {
1869 RESUMED.swap(false, Ordering::SeqCst)
1870 }
1871}
1872
1873#[cfg(test)]
1874mod tests {
1875 use super::*;
1876
1877 #[test]
1878 fn backends_are_named() {
1879 assert_eq!(BackendKind::default(), BackendKind::Crossterm);
1880 assert_eq!(BackendKind::ALL[0], BackendKind::Crossterm);
1881 for kind in BackendKind::ALL {
1882 assert_eq!(BackendKind::from_name(kind.name()), Some(*kind));
1883 assert_eq!(
1884 BackendKind::from_name(&kind.name().to_uppercase()),
1885 Some(*kind)
1886 );
1887 assert_eq!(kind.to_string(), kind.name());
1888 }
1889 assert_eq!(BackendKind::from_name("curses"), None);
1890 assert_eq!(
1891 BackendKind::ALL.len(),
1892 1 + usize::from(cfg!(all(unix, feature = "termion")))
1893 + usize::from(cfg!(feature = "termwiz"))
1894 );
1895 }
1896
1897 #[test]
1898 fn every_backend_leaves_the_same_modes() {
1899 let all = RAW | ALTERNATE | MOUSE | PASTE | KITTY;
1900 let expected =
1901 "\x1b[<1u\x1b[?1006l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h";
1902 for kind in BackendKind::ALL {
1903 let out = undo(all, *kind);
1904 assert!(out.starts_with(expected), "{kind}: {out:?}");
1905 let rest = &out[expected.len()..];
1908 assert!(rest == "\x1b[?1049l" || rest.is_empty(), "{kind}: {rest:?}");
1909 assert_eq!(rest.is_empty(), kind.owns_alternate_screen());
1910 }
1911 }
1912
1913 #[test]
1914 fn answers_are_stripped_from_what_was_typed() {
1915 assert_eq!(strip_answers(b"\x1b[?2026;2$yq\x1b[?62c"), b"q");
1916 assert_eq!(strip_answers(b"ab\x1b[?62"), b"ab");
1917 assert_eq!(strip_answers(b"\x1b[A\x1b"), b"\x1b[A\x1b");
1918 assert!(strip_answers(b"\x1b[?0u\x1b[?62c").is_empty());
1919 }
1920
1921 #[test]
1922 fn private_answers_are_found() {
1923 let lasts = |bytes: &[u8]| -> Vec<u8> { private_answers(bytes).map(|a| a.last).collect() };
1924 assert_eq!(lasts(b"x\x1b[?0u\x1b[?62;22c"), b"uc");
1925 assert_eq!(lasts(b"\x1b[?62"), b"");
1926 let decrqm: Vec<_> = private_answers(b"\x1b[?2026;2$y").collect();
1927 assert_eq!(
1928 decrqm,
1929 [PrivateAnswer {
1930 parameters: b"2026;2",
1931 intermediates: b"$",
1932 last: b'y',
1933 }]
1934 );
1935 assert_eq!(lasts(b"\x1b[?2026\x1b[?62c"), b"c");
1938 }
1939
1940 #[test]
1941 fn the_start_up_answers_are_read() {
1942 let read = |bytes: &[u8]| Answers::read(bytes);
1943 let both = Answers {
1944 kitty: true,
1945 sync: true,
1946 pixels: false,
1947 };
1948 let sync = Answers {
1949 sync: true,
1950 ..Answers::default()
1951 };
1952 assert_eq!(read(b"\x1b[?2026;2$y\x1b[?0u\x1b[?62;22c"), Some(both));
1954 assert_eq!(read(b"\x1b[?2026;1$y\x1b[?62c"), Some(sync));
1955 assert_eq!(read(b"\x1b[?2026;3$y\x1b[?62c"), Some(sync));
1956 assert_eq!(read(b"\x1b[?2026;0$y\x1b[?62c"), Some(Answers::default()));
1958 assert_eq!(read(b"\x1b[?2026;4$y\x1b[?62c"), Some(Answers::default()));
1959 assert_eq!(
1961 read(b"\x1b[?1u\x1b[?62c"),
1962 Some(Answers {
1963 kitty: true,
1964 ..Answers::default()
1965 })
1966 );
1967 assert_eq!(read(b"\x1b[?2004;1$y\x1b[?62c"), Some(Answers::default()));
1969 assert_eq!(read(b""), None);
1972 assert_eq!(read(b"\x1b[?2026;2$y\x1b[?0u"), None);
1973 assert_eq!(read(b"\x1b[?2026;2$y\x1b[?6"), None);
1974 assert_eq!(
1977 read(b"j\x1b[A\x1b[?2026;2$yk\x1b[?0u\r\x1b[?62;22c"),
1978 Some(both)
1979 );
1980 let pixels = Answers {
1982 pixels: true,
1983 ..Answers::default()
1984 };
1985 let all = Answers {
1986 pixels: true,
1987 ..both
1988 };
1989 assert_eq!(read(b"\x1b[?1016;2$y\x1b[?62c"), Some(pixels));
1990 assert_eq!(read(b"\x1b[?1016;1$y\x1b[?62c"), Some(pixels));
1991 assert_eq!(read(b"\x1b[?1016;0$y\x1b[?62c"), Some(Answers::default()));
1992 assert_eq!(read(b"\x1b[?1016;4$y\x1b[?62c"), Some(Answers::default()));
1993 assert_eq!(
1994 read(b"\x1b[?2026;2$y\x1b[?1016;2$y\x1b[?0u\x1b[?62;22c"),
1995 Some(all)
1996 );
1997 assert_eq!(read(b"\x1b[?2026;2$y\x1b[?62c"), Some(sync));
1999 for answers in [Answers::default(), sync, both, pixels, all] {
2000 assert_ne!(answers.encode(), 0);
2001 assert_eq!(Answers::decode(answers.encode()), answers);
2002 }
2003 }
2004
2005 #[test]
2006 fn the_cell_size_comes_from_the_window_size_in_pixels() {
2007 assert_eq!(cell_of(80, 24, 800, 480), Some((10, 20)));
2008 assert_eq!(cell_of(80, 24, 805, 490), Some((10, 20)));
2010 assert_eq!(cell_of(80, 24, 0, 0), None);
2012 assert_eq!(cell_of(0, 0, 800, 480), None);
2013 assert_eq!(cell_of(80, 24, 40, 480), None);
2014 }
2015
2016 #[test]
2017 fn pixel_reports_are_allowed_unless_turned_off() {
2018 assert!(pixels_allowed(None));
2019 assert!(pixels_allowed(Some("1")));
2020 assert!(!pixels_allowed(Some("0")));
2021 assert!(!pixels_allowed(Some(" 0 ")));
2022 }
2023
2024 #[test]
2025 fn the_restore_turns_pixel_reports_off_before_the_mouse() {
2026 for kind in BackendKind::ALL {
2027 let out = undo(RAW | MOUSE | PIXELS, *kind);
2028 assert!(out.starts_with("\x1b[?1016l\x1b[?1006l"), "{kind}: {out:?}");
2029 assert!(!undo(RAW | MOUSE, *kind).contains("1016"));
2030 }
2031 }
2032
2033 #[test]
2034 fn a_cursor_report_is_found_among_other_input() {
2035 assert_eq!(cursor_report(b"\x1b[12;1R"), Some((11, 0..7)));
2036 assert_eq!(cursor_report(b"\x1b[A\x1b[12;1R"), Some((11, 3..10)));
2039 assert_eq!(
2040 cursor_report(b"\x1b[<0;2;13M\x1b[3;40Rx"),
2041 Some((2, 10..17))
2042 );
2043 assert_eq!(cursor_report(b"\x1b[1;\x1b[5;1R"), Some((4, 4..10)));
2044 for bytes in [
2046 &b"\x1b[12;"[..],
2047 b"\x1b[12R",
2048 b"\x1b[;1R",
2049 b"\x1b[1;5C",
2050 b"12;1R",
2051 ] {
2052 assert_eq!(cursor_report(bytes), None, "{bytes:?}");
2053 }
2054 }
2055
2056 #[test]
2057 fn the_environment_forces_synchronized_output() {
2058 assert_eq!(sync_override(Some("0")), Some(false));
2059 assert_eq!(sync_override(Some("1")), Some(true));
2060 assert_eq!(sync_override(Some(" 1\n")), Some(true));
2061 assert_eq!(sync_override(None), None);
2062 assert_eq!(sync_override(Some("")), None);
2063 assert_eq!(sync_override(Some("auto")), None);
2064 }
2065
2066 #[test]
2067 fn a_frame_is_one_synchronized_update_and_an_empty_one_nothing() {
2068 assert_eq!(
2069 synchronized_update("\x1b[2;1Hhi\x1b[?25l"),
2070 "\x1b[?2026h\x1b[2;1Hhi\x1b[?25l\x1b[?2026l"
2071 );
2072 assert_eq!(synchronized_update(""), "");
2073 }
2074
2075 #[test]
2076 fn the_restore_ends_a_synchronized_update_first() {
2077 for kind in BackendKind::ALL {
2078 let out = undo(RAW | SYNC | MOUSE, *kind);
2079 assert!(out.starts_with("\x1b[?2026l\x1b[?1006l"), "{kind}: {out:?}");
2080 assert_eq!(out.matches("2026").count(), 1);
2081 assert!(!undo(RAW | MOUSE, *kind).contains("2026"));
2083 }
2084 }
2085}