1use std::{
4 fs::File,
5 io::{self, Write as _},
6 panic,
7 sync::{
8 Arc, LazyLock, Once,
9 atomic::{AtomicBool, AtomicU8, AtomicU16, AtomicU64, Ordering},
10 },
11 thread,
12 time::{Duration, Instant},
13};
14
15use omp_core::{Str, base64, fmts};
16use smallvec::SmallVec;
17#[cfg(windows)]
18use windows_sys::Win32::System::Console::{GetConsoleOutputCP, SetConsoleOutputCP};
19
20const STDERR_CAPTURE_CAPACITY: usize = 64 * 1024;
21
22#[derive(Default)]
23struct CapturedStderr {
24 bytes: Vec<u8>,
25}
26
27impl CapturedStderr {
28 fn new() -> Self {
29 Self { bytes: Vec::with_capacity(STDERR_CAPTURE_CAPACITY) }
30 }
31
32 fn push(&mut self, bytes: &[u8]) {
33 if bytes.len() >= STDERR_CAPTURE_CAPACITY {
34 self.bytes.clear();
35 self
36 .bytes
37 .extend_from_slice(&bytes[bytes.len() - STDERR_CAPTURE_CAPACITY..]);
38 return;
39 }
40 let overflow = self
41 .bytes
42 .len()
43 .saturating_add(bytes.len())
44 .saturating_sub(STDERR_CAPTURE_CAPACITY);
45 if overflow != 0 {
46 self.bytes.copy_within(overflow.., 0);
47 self.bytes.truncate(self.bytes.len() - overflow);
48 }
49 self.bytes.extend_from_slice(bytes);
50 }
51
52 fn as_slice(&self) -> &[u8] {
53 &self.bytes
54 }
55}
56
57#[cfg(unix)]
58mod platform {
59 use std::{
60 cell::UnsafeCell,
61 fs::{File, OpenOptions},
62 io,
63 mem::MaybeUninit,
64 os::fd::{AsRawFd as _, FromRawFd as _, OwnedFd, RawFd},
65 sync::atomic::{AtomicBool, AtomicI32, Ordering},
66 time::{Duration, Instant},
67 };
68
69 use nix::{
70 libc,
71 sys::{
72 signal::{self, SaFlags, SigAction, SigHandler, SigSet, Signal},
73 termios::{SetArg, Termios, cfmakeraw, tcgetattr, tcsetattr},
74 },
75 };
76
77 use super::{CapturedStderr, RESIZE_GENERATION, emergency_restore_inner};
78 use crate::Size;
79
80 static TTY_FD: AtomicI32 = AtomicI32::new(-1);
81 static RAW_VALID: AtomicBool = AtomicBool::new(false);
82 static SAVED_STDERR_FD: AtomicI32 = AtomicI32::new(-1);
83
84 static RESIZE_PIPE_FDS: (AtomicI32, AtomicI32) = (AtomicI32::new(-1), AtomicI32::new(-1));
85 struct SavedTermios(UnsafeCell<MaybeUninit<libc::termios>>);
86
87 unsafe impl Sync for SavedTermios {}
91
92 static SIGNAL_TERMIOS: SavedTermios = SavedTermios(UnsafeCell::new(MaybeUninit::uninit()));
93
94 pub(super) struct State {
95 original: Option<Termios>,
96 }
97 pub(super) struct StderrGuard {
98 reader: Option<File>,
99 captured: CapturedStderr,
100 active: bool,
101 }
102
103 impl StderrGuard {
104 pub(super) fn new(capture: bool) -> io::Result<Self> {
105 if !capture {
106 return Ok(Self {
107 reader: None,
108 captured: CapturedStderr::default(),
109 active: false,
110 });
111 }
112
113 let mut descriptors = [-1; 2];
114 if unsafe { libc::pipe(descriptors.as_mut_ptr()) } != 0 {
116 return Err(io::Error::last_os_error());
117 }
118 let (reader, writer) = (descriptors[0], descriptors[1]);
119 if let Err(error) = configure_pipe(reader, writer) {
120 unsafe {
122 libc::close(reader);
123 libc::close(writer);
124 }
125 return Err(error);
126 }
127 let saved = unsafe { libc::dup(libc::STDERR_FILENO) };
129 if saved < 0 {
130 let error = io::Error::last_os_error();
131 unsafe {
133 libc::close(reader);
134 libc::close(writer);
135 }
136 return Err(error);
137 }
138 let entry_backup = unsafe { libc::dup(saved) };
140 if entry_backup < 0 {
141 let error = io::Error::last_os_error();
142 unsafe {
144 libc::close(reader);
145 libc::close(writer);
146 libc::close(saved);
147 }
148 return Err(error);
149 }
150 for descriptor in [saved, entry_backup] {
151 if unsafe { libc::fcntl(descriptor, libc::F_SETFD, libc::FD_CLOEXEC) } < 0 {
153 let error = io::Error::last_os_error();
154 unsafe {
156 libc::close(reader);
157 libc::close(writer);
158 libc::close(saved);
159 libc::close(entry_backup);
160 }
161 return Err(error);
162 }
163 }
164
165 SAVED_STDERR_FD.store(saved, Ordering::Release);
168 if unsafe { libc::dup2(writer, libc::STDERR_FILENO) } < 0 {
170 let error = io::Error::last_os_error();
171 if SAVED_STDERR_FD
172 .compare_exchange(saved, -1, Ordering::AcqRel, Ordering::Acquire)
173 .is_ok()
174 {
175 unsafe {
177 libc::close(saved);
178 }
179 }
180 unsafe {
182 libc::close(reader);
183 libc::close(writer);
184 libc::close(entry_backup);
185 }
186 return Err(error);
187 }
188 if SAVED_STDERR_FD.load(Ordering::Acquire) != saved {
189 unsafe {
193 libc::dup2(entry_backup, libc::STDERR_FILENO);
194 libc::close(reader);
195 libc::close(writer);
196 libc::close(entry_backup);
197 }
198 return Err(io::Error::new(
199 io::ErrorKind::Interrupted,
200 "stderr capture interrupted by emergency restore",
201 ));
202 }
203 unsafe {
205 libc::close(writer);
206 libc::close(entry_backup);
207 }
208 Ok(Self {
209 reader: Some(unsafe { File::from_raw_fd(reader) }),
211 captured: CapturedStderr::new(),
212 active: true,
213 })
214 }
215
216 pub(super) fn drain(&mut self) {
217 let Some(reader) = &self.reader else {
218 return;
219 };
220 let mut chunk = [0_u8; 4096];
221 loop {
222 let count =
224 unsafe { libc::read(reader.as_raw_fd(), chunk.as_mut_ptr().cast(), chunk.len()) };
225 if count > 0 {
226 self.captured.push(&chunk[..count as usize]);
227 continue;
228 }
229 if count == 0 {
230 break;
231 }
232 let error = io::Error::last_os_error();
233 if error.kind() == io::ErrorKind::Interrupted {
234 continue;
235 }
236 break;
237 }
238 }
239
240 pub(super) fn restore(&mut self) -> io::Result<()> {
241 let result = if self.active {
242 restore_stderr()
243 } else {
244 Ok(())
245 };
246 if result.is_ok() {
247 self.active = false;
248 }
249 self.drain();
250 result
251 }
252
253 pub(super) fn captured(&self) -> &[u8] {
254 self.captured.as_slice()
255 }
256 }
257
258 impl Drop for StderrGuard {
259 fn drop(&mut self) {
260 let _ = self.restore();
261 }
262 }
263
264 fn configure_pipe(reader: RawFd, writer: RawFd) -> io::Result<()> {
265 for descriptor in [reader, writer] {
266 if unsafe { libc::fcntl(descriptor, libc::F_SETFD, libc::FD_CLOEXEC) } < 0 {
268 return Err(io::Error::last_os_error());
269 }
270 let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFL) };
272 if flags < 0
273 || unsafe { libc::fcntl(descriptor, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0
275 {
276 return Err(io::Error::last_os_error());
277 }
278 }
279 Ok(())
280 }
281 pub(super) fn activate_resize_pipe() -> io::Result<()> {
282 let mut descriptors = [-1; 2];
283 if unsafe { libc::pipe(descriptors.as_mut_ptr()) } != 0 {
285 return Err(io::Error::last_os_error());
286 }
287 if let Err(error) = configure_pipe(descriptors[0], descriptors[1]) {
288 unsafe {
290 libc::close(descriptors[0]);
291 libc::close(descriptors[1]);
292 }
293 return Err(error);
294 }
295 RESIZE_PIPE_FDS.0.store(descriptors[0], Ordering::Release);
296 RESIZE_PIPE_FDS.1.store(descriptors[1], Ordering::Release);
297 Ok(())
298 }
299 pub(super) fn resize_pipe_reader() -> io::Result<OwnedFd> {
300 let reader = RESIZE_PIPE_FDS.0.load(Ordering::Acquire);
301 if reader < 0 {
302 return Err(io::Error::new(io::ErrorKind::NotConnected, "resize pipe is inactive"));
303 }
304 let duplicate = unsafe { libc::fcntl(reader, libc::F_DUPFD_CLOEXEC, 0) };
306 if duplicate < 0 {
307 return Err(io::Error::last_os_error());
308 }
309 Ok(unsafe { OwnedFd::from_raw_fd(duplicate) })
311 }
312
313 fn close_resize_pipe() {
314 let writer = RESIZE_PIPE_FDS.1.swap(-1, Ordering::AcqRel);
315 let reader = RESIZE_PIPE_FDS.0.swap(-1, Ordering::AcqRel);
316 unsafe {
319 if writer >= 0 {
320 libc::close(writer);
321 }
322 if reader >= 0 {
323 libc::close(reader);
324 }
325 }
326 }
327
328 fn restore_stderr() -> io::Result<()> {
329 loop {
330 let saved = SAVED_STDERR_FD.load(Ordering::Acquire);
331 if saved < 0 {
332 return Ok(());
333 }
334 if unsafe { libc::dup2(saved, libc::STDERR_FILENO) } < 0 {
336 let error = io::Error::last_os_error();
337 if error.kind() == io::ErrorKind::Interrupted {
338 continue;
339 }
340 if SAVED_STDERR_FD.load(Ordering::Acquire) < 0 {
343 return Ok(());
344 }
345 return Err(error);
346 }
347 if SAVED_STDERR_FD
348 .compare_exchange(saved, -1, Ordering::AcqRel, Ordering::Acquire)
349 .is_ok()
350 {
351 unsafe {
353 libc::close(saved);
354 }
355 }
356 return Ok(());
357 }
358 }
359
360 pub(super) fn emergency_restore_stderr() {
361 let saved = SAVED_STDERR_FD.swap(-1, Ordering::AcqRel);
362 if saved < 0 {
363 return;
364 }
365 loop {
366 if unsafe { libc::dup2(saved, libc::STDERR_FILENO) } >= 0 || errno() != libc::EINTR {
368 break;
369 }
370 }
371 unsafe {
373 libc::close(saved);
374 }
375 }
376
377 pub(super) fn prepare() -> io::Result<(File, State)> {
378 let tty = crate::tty::open(OpenOptions::new().read(true).write(true))?;
379 let original = tcgetattr(&tty).map_err(errno_to_io)?;
380 Ok((tty, State { original: Some(original) }))
381 }
382
383 pub(super) fn enable_raw(tty: &File, state: &State) -> io::Result<()> {
384 let original = state
385 .original
386 .as_ref()
387 .expect("prepared terminal has original mode");
388 let mut signal_termios = MaybeUninit::<libc::termios>::uninit();
389 if unsafe { libc::tcgetattr(tty.as_raw_fd(), signal_termios.as_mut_ptr()) } != 0 {
392 return Err(io::Error::last_os_error());
393 }
394 unsafe { (*SIGNAL_TERMIOS.0.get()).write(signal_termios.assume_init()) };
396 TTY_FD.store(tty.as_raw_fd(), Ordering::Release);
397 RAW_VALID.store(true, Ordering::Release);
398
399 let mut raw = original.clone();
400 cfmakeraw(&mut raw);
401 if let Err(error) = tcsetattr(tty, SetArg::TCSANOW, &raw) {
402 let _ = tcsetattr(tty, SetArg::TCSANOW, original);
403 deactivate();
404 return Err(errno_to_io(error));
405 }
406 Ok(())
407 }
408
409 pub(super) fn restore_raw(tty: &File, state: &mut State) -> io::Result<()> {
410 let Some(original) = &state.original else {
411 return Ok(());
412 };
413 tcsetattr(tty, SetArg::TCSAFLUSH, original).map_err(errno_to_io)?;
416 state.original = None;
417 Ok(())
418 }
419
420 pub(super) fn size(tty: &File, _: &State) -> io::Result<Size> {
421 let mut window = MaybeUninit::<libc::winsize>::zeroed();
422 if unsafe { libc::ioctl(tty.as_raw_fd(), libc::TIOCGWINSZ, window.as_mut_ptr()) } != 0 {
424 return Err(io::Error::last_os_error());
425 }
426 let window = unsafe { window.assume_init() };
428 if window.ws_col == 0 || window.ws_row == 0 {
429 return Err(io::Error::new(io::ErrorKind::InvalidData, "terminal reported a zero size"));
430 }
431 Ok(Size::new(window.ws_col, window.ws_row))
432 }
433
434 fn input_fd(tty: &File) -> RawFd {
438 #[cfg(not(test))]
439 if !crate::tty::overridden() {
440 return libc::STDIN_FILENO;
441 }
442 tty.as_raw_fd()
443 }
444
445 pub(super) fn drain(tty: &File, _: &State, maximum: Duration, idle: Duration) -> io::Result<()> {
446 drain_fd(input_fd(tty), maximum, idle)
447 }
448
449 fn drain_fd(fd: RawFd, maximum: Duration, idle: Duration) -> io::Result<()> {
450 let started = Instant::now();
451 let mut last_data = started;
452 let mut buffer = [0; 256];
453 loop {
454 let now = Instant::now();
455 let wait = maximum
456 .saturating_sub(now.duration_since(started))
457 .min(idle.saturating_sub(now.duration_since(last_data)));
458 if wait.is_zero() {
459 return Ok(());
460 }
461 let timeout = wait.as_millis().clamp(1, i32::MAX as u128) as i32;
462 let mut descriptor = libc::pollfd { fd, events: libc::POLLIN, revents: 0 };
463 let result = unsafe { libc::poll(&mut descriptor, 1, timeout) };
465 if result == 0 {
466 continue;
467 }
468 if result < 0 {
469 let error = io::Error::last_os_error();
470 if error.kind() == io::ErrorKind::Interrupted {
471 continue;
472 }
473 return Err(error);
474 }
475 if descriptor.revents & (libc::POLLIN | libc::POLLHUP) == 0 {
476 continue;
477 }
478 let read = unsafe { libc::read(fd, buffer.as_mut_ptr().cast(), buffer.len()) };
481 if read > 0 {
482 last_data = Instant::now();
483 continue;
484 }
485 if read == 0 {
486 return Ok(());
487 }
488 let error = io::Error::last_os_error();
489 if !matches!(error.kind(), io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock) {
490 return Err(error);
491 }
492 }
493 }
494
495 pub(super) fn install_handlers() -> Result<(), i32> {
496 let fatal = SigAction::new(
497 SigHandler::Handler(fatal_signal_handler),
498 SaFlags::SA_RESTART,
499 SigSet::empty(),
500 );
501 let resize = SigAction::new(
502 SigHandler::Handler(resize_signal_handler),
503 SaFlags::SA_RESTART,
504 SigSet::empty(),
505 );
506 unsafe {
509 signal::sigaction(Signal::SIGINT, &fatal).map_err(|error| error as i32)?;
510 signal::sigaction(Signal::SIGTERM, &fatal).map_err(|error| error as i32)?;
511 signal::sigaction(Signal::SIGHUP, &fatal).map_err(|error| error as i32)?;
512 signal::sigaction(Signal::SIGWINCH, &resize).map_err(|error| error as i32)?;
513 }
514 Ok(())
515 }
516
517 pub(super) fn notify_resize_pipe() {
519 let fd = RESIZE_PIPE_FDS.1.load(Ordering::Acquire);
520 if fd >= 0 {
521 unsafe {
523 libc::write(fd, b"r".as_ptr().cast(), 1);
524 }
525 }
526 }
527
528 extern "C" fn resize_signal_handler(_: libc::c_int) {
529 RESIZE_GENERATION.fetch_add(1, Ordering::Relaxed);
530 notify_resize_pipe();
531 }
532
533 extern "C" fn fatal_signal_handler(signal_number: libc::c_int) {
534 emergency_restore_inner();
535 unsafe {
538 libc::signal(signal_number, libc::SIG_DFL);
539 libc::raise(signal_number);
540 }
541 }
542
543 pub(super) fn emergency_restore(payloads: [&[u8]; 3]) {
544 close_resize_pipe();
545 let fd = TTY_FD.load(Ordering::Acquire);
546 if fd < 0 {
547 return;
548 }
549 for payload in payloads {
550 raw_write_all(fd, payload);
551 }
552 if RAW_VALID.swap(false, Ordering::AcqRel) {
553 let termios = unsafe { (*SIGNAL_TERMIOS.0.get()).assume_init_ref() };
556 unsafe {
558 libc::tcsetattr(fd, libc::TCSANOW, termios);
559 libc::tcflush(fd, libc::TCIFLUSH);
563 }
564 }
565 }
566
567 fn raw_write_all(fd: RawFd, mut bytes: &[u8]) {
568 while !bytes.is_empty() {
569 let written = unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) };
571 if written > 0 {
572 bytes = &bytes[written as usize..];
573 continue;
574 }
575 if written < 0 && errno() == libc::EINTR {
576 continue;
577 }
578 break;
579 }
580 }
581
582 #[cfg(any(
583 target_os = "macos",
584 target_os = "ios",
585 target_os = "freebsd",
586 target_os = "openbsd",
587 target_os = "netbsd",
588 target_os = "dragonfly"
589 ))]
590 fn errno() -> libc::c_int {
591 unsafe { *libc::__error() }
593 }
594
595 #[cfg(any(target_os = "linux", target_os = "android"))]
596 fn errno() -> libc::c_int {
597 unsafe { *libc::__errno_location() }
599 }
600
601 pub(super) fn deactivate() {
602 close_resize_pipe();
603 RAW_VALID.store(false, Ordering::Release);
604 TTY_FD.store(-1, Ordering::Release);
605 }
606
607 fn errno_to_io(error: nix::errno::Errno) -> io::Error {
608 io::Error::from_raw_os_error(error as i32)
609 }
610
611 #[cfg(test)]
612 pub(super) fn drain_for_test(fd: RawFd, maximum: Duration, idle: Duration) -> io::Result<()> {
613 drain_fd(fd, maximum, idle)
614 }
615
616 #[cfg(test)]
617 pub(super) const fn state_for_test() -> State {
618 State { original: None }
619 }
620}
621
622#[cfg(windows)]
623mod platform {
624 use std::{
625 ffi::c_void,
626 fs::{File, OpenOptions, remove_file},
627 io::{self, Read as _},
628 os::windows::io::AsRawHandle as _,
629 path::PathBuf,
630 ptr,
631 sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering},
632 thread,
633 time::{Duration, Instant},
634 };
635
636 use windows_sys::Win32::{
637 Foundation::{FALSE, HANDLE, TRUE},
638 System::Console::{
639 CONSOLE_SCREEN_BUFFER_INFO, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT,
640 ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode,
641 GetConsoleScreenBufferInfo, GetNumberOfConsoleInputEvents, GetStdHandle, INPUT_RECORD,
642 ReadConsoleInputW, STD_ERROR_HANDLE, SetConsoleCtrlHandler, SetConsoleMode, SetStdHandle,
643 WriteConsoleA,
644 },
645 };
646
647 use super::{CapturedStderr, emergency_restore_inner};
648 use crate::Size;
649 static INPUT_HANDLE: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
650 static OUTPUT_HANDLE: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
651 static INPUT_MODE: AtomicU32 = AtomicU32::new(0);
652 static OUTPUT_MODE: AtomicU32 = AtomicU32::new(0);
653 static MODES_VALID: AtomicBool = AtomicBool::new(false);
654 static SAVED_STDERR_HANDLE: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
655 static STDERR_HANDLE_VALID: AtomicBool = AtomicBool::new(false);
656 static STDERR_FILE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
657
658 pub(super) struct State {
659 _input_file: File,
660 input: HANDLE,
661 output: HANDLE,
662 original_input: u32,
663 original_output: u32,
664 raw: bool,
665 }
666 pub(super) struct StderrGuard {
667 writer: Option<File>,
668 reader: Option<File>,
669 path: Option<PathBuf>,
670 captured: CapturedStderr,
671 active: bool,
672 }
673
674 impl StderrGuard {
675 pub(super) fn new(capture: bool) -> io::Result<Self> {
676 if !capture {
677 return Ok(Self {
678 writer: None,
679 reader: None,
680 path: None,
681 captured: CapturedStderr::default(),
682 active: false,
683 });
684 }
685 let sequence = STDERR_FILE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
686 let path = std::env::temp_dir()
687 .join(format!("omp-tui-stderr-{}-{sequence}.tmp", std::process::id()));
688 let writer = OpenOptions::new()
689 .write(true)
690 .create_new(true)
691 .open(&path)?;
692 let reader = match OpenOptions::new().read(true).open(&path) {
693 Ok(reader) => reader,
694 Err(error) => {
695 drop(writer);
696 let _ = remove_file(&path);
697 return Err(error);
698 },
699 };
700 let original = unsafe { GetStdHandle(STD_ERROR_HANDLE) };
701 SAVED_STDERR_HANDLE.store(original, Ordering::Release);
702 STDERR_HANDLE_VALID.store(true, Ordering::Release);
703 if unsafe { SetStdHandle(STD_ERROR_HANDLE, writer.as_raw_handle()) } == 0 {
704 let error = io::Error::last_os_error();
705 STDERR_HANDLE_VALID.store(false, Ordering::Release);
706 SAVED_STDERR_HANDLE.store(ptr::null_mut(), Ordering::Release);
707 drop(reader);
708 drop(writer);
709 let _ = remove_file(&path);
710 return Err(error);
711 }
712 Ok(Self {
713 writer: Some(writer),
714 reader: Some(reader),
715 path: Some(path),
716 captured: CapturedStderr::new(),
717 active: true,
718 })
719 }
720
721 pub(super) fn drain(&mut self) {
722 let Some(reader) = &mut self.reader else {
723 return;
724 };
725 let mut chunk = [0_u8; 4096];
726 loop {
727 match reader.read(&mut chunk) {
728 Ok(0) | Err(_) => break,
729 Ok(count) => self.captured.push(&chunk[..count]),
730 }
731 }
732 }
733
734 pub(super) fn restore(&mut self) -> io::Result<()> {
735 let result = if self.active {
736 restore_stderr()
737 } else {
738 Ok(())
739 };
740 if result.is_ok() {
741 self.active = false;
742 self.writer.take();
743 }
744 self.drain();
745 result
746 }
747
748 pub(super) fn captured(&self) -> &[u8] {
749 self.captured.as_slice()
750 }
751 }
752
753 impl Drop for StderrGuard {
754 fn drop(&mut self) {
755 let _ = self.restore();
756 self.reader.take();
757 if let Some(path) = self.path.take() {
758 let _ = remove_file(path);
759 }
760 }
761 }
762
763 fn restore_stderr() -> io::Result<()> {
764 if !STDERR_HANDLE_VALID.load(Ordering::Acquire) {
765 return Ok(());
766 }
767 let original = SAVED_STDERR_HANDLE.load(Ordering::Acquire);
768 if unsafe { SetStdHandle(STD_ERROR_HANDLE, original) } == 0 {
769 return Err(io::Error::last_os_error());
770 }
771 STDERR_HANDLE_VALID.store(false, Ordering::Release);
772 SAVED_STDERR_HANDLE.store(ptr::null_mut(), Ordering::Release);
773 Ok(())
774 }
775
776 pub(super) fn emergency_restore_stderr() {
777 if !STDERR_HANDLE_VALID.swap(false, Ordering::AcqRel) {
778 return;
779 }
780 let original = SAVED_STDERR_HANDLE.swap(ptr::null_mut(), Ordering::AcqRel);
781 unsafe {
782 SetStdHandle(STD_ERROR_HANDLE, original);
783 }
784 }
785
786 pub(super) fn prepare() -> io::Result<(File, State)> {
787 let tty = OpenOptions::new().write(true).open("CONOUT$")?;
788 let input_file = OpenOptions::new().read(true).open("CONIN$")?;
789 let input = input_file.as_raw_handle();
790 let output = tty.as_raw_handle();
791 let mut original_input = 0;
792 let mut original_output = 0;
793 if unsafe { GetConsoleMode(input, &mut original_input) } == 0
794 || unsafe { GetConsoleMode(output, &mut original_output) } == 0
795 {
796 return Err(io::Error::last_os_error());
797 }
798 Ok((tty, State {
799 _input_file: input_file,
800 input,
801 output,
802 original_input,
803 original_output,
804 raw: false,
805 }))
806 }
807
808 pub(super) fn enable_raw(_: &File, state: &mut State) -> io::Result<()> {
809 let input_mode = (state.original_input | ENABLE_VIRTUAL_TERMINAL_INPUT)
810 & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
811 if unsafe { SetConsoleMode(state.input, input_mode) } == 0 {
812 return Err(io::Error::last_os_error());
813 }
814 let output_mode = state.original_output | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
815 if unsafe { SetConsoleMode(state.output, output_mode) } == 0 {
816 let error = io::Error::last_os_error();
817 let _ = unsafe { SetConsoleMode(state.input, state.original_input) };
818 return Err(error);
819 }
820 INPUT_HANDLE.store(state.input, Ordering::Release);
821 OUTPUT_HANDLE.store(state.output, Ordering::Release);
822 INPUT_MODE.store(state.original_input, Ordering::Release);
823 OUTPUT_MODE.store(state.original_output, Ordering::Release);
824 MODES_VALID.store(true, Ordering::Release);
825 state.raw = true;
826 Ok(())
827 }
828
829 pub(super) fn restore_raw(_: &File, state: &mut State) -> io::Result<()> {
830 if !state.raw {
831 return Ok(());
832 }
833 let mut first = None;
834 if unsafe { SetConsoleMode(state.input, state.original_input) } == 0 {
835 first = Some(io::Error::last_os_error());
836 }
837 if unsafe { SetConsoleMode(state.output, state.original_output) } == 0 && first.is_none() {
838 first = Some(io::Error::last_os_error());
839 }
840 if first.is_none() {
841 state.raw = false;
842 MODES_VALID.store(false, Ordering::Release);
843 }
844 first.map_or(Ok(()), Err)
845 }
846
847 pub(super) fn size(_: &File, state: &State) -> io::Result<Size> {
848 let mut info = unsafe { std::mem::zeroed::<CONSOLE_SCREEN_BUFFER_INFO>() };
849 if unsafe { GetConsoleScreenBufferInfo(state.output, &mut info) } == 0 {
850 return Err(io::Error::last_os_error());
851 }
852 let columns = i32::from(info.srWindow.Right) - i32::from(info.srWindow.Left) + 1;
853 let rows = i32::from(info.srWindow.Bottom) - i32::from(info.srWindow.Top) + 1;
854 let columns = u16::try_from(columns).map_err(|_| {
855 io::Error::new(io::ErrorKind::InvalidData, "console reported an invalid width")
856 })?;
857 let rows = u16::try_from(rows).map_err(|_| {
858 io::Error::new(io::ErrorKind::InvalidData, "console reported an invalid height")
859 })?;
860 if columns == 0 || rows == 0 {
861 return Err(io::Error::new(io::ErrorKind::InvalidData, "console reported a zero size"));
862 }
863 Ok(Size::new(columns, rows))
864 }
865
866 pub(super) const fn activate_resize_pipe() -> io::Result<()> {
867 Ok(())
868 }
869
870 pub(super) fn drain(
871 _: &File,
872 state: &State,
873 maximum: Duration,
874 idle: Duration,
875 ) -> io::Result<()> {
876 let started = Instant::now();
877 let mut last_data = started;
878 let mut records = [unsafe { std::mem::zeroed::<INPUT_RECORD>() }; 64];
879 loop {
880 let now = Instant::now();
881 let wait = maximum
882 .saturating_sub(now.duration_since(started))
883 .min(idle.saturating_sub(now.duration_since(last_data)));
884 if wait.is_zero() {
885 return Ok(());
886 }
887 let mut available = 0;
888 if unsafe { GetNumberOfConsoleInputEvents(state.input, &mut available) } == 0 {
889 return Err(io::Error::last_os_error());
890 }
891 if available == 0 {
892 thread::sleep(wait.min(Duration::from_millis(1)));
893 continue;
894 }
895 let mut read = 0;
896 let count = available.min(records.len() as u32);
897 if unsafe { ReadConsoleInputW(state.input, records.as_mut_ptr(), count, &mut read) } == 0 {
898 return Err(io::Error::last_os_error());
899 }
900 if read != 0 {
901 last_data = Instant::now();
902 }
903 }
904 }
905
906 pub(super) fn install_handlers() -> Result<(), i32> {
907 if unsafe { SetConsoleCtrlHandler(Some(console_ctrl_handler), TRUE) } == 0 {
908 return Err(io::Error::last_os_error().raw_os_error().unwrap_or(1));
909 }
910 Ok(())
911 }
912
913 unsafe extern "system" fn console_ctrl_handler(_: u32) -> i32 {
914 emergency_restore_inner();
915 FALSE
916 }
917
918 pub(super) fn emergency_restore(payloads: [&[u8]; 3]) {
919 if !MODES_VALID.swap(false, Ordering::AcqRel) {
920 return;
921 }
922 let output = OUTPUT_HANDLE.load(Ordering::Acquire);
923 for mut remaining in payloads {
924 while !remaining.is_empty() {
925 let mut written = 0;
926 if unsafe {
927 WriteConsoleA(
928 output,
929 remaining.as_ptr(),
930 remaining.len().min(u32::MAX as usize) as u32,
931 &mut written,
932 ptr::null(),
933 )
934 } == 0 || written == 0
935 {
936 break;
937 }
938 remaining = &remaining[written as usize..];
939 }
940 }
941 let input = INPUT_HANDLE.load(Ordering::Acquire);
942 let _ = unsafe { SetConsoleMode(input, INPUT_MODE.load(Ordering::Acquire)) };
943 let _ = unsafe { SetConsoleMode(output, OUTPUT_MODE.load(Ordering::Acquire)) };
944 }
945
946 pub(super) fn deactivate() {
947 MODES_VALID.store(false, Ordering::Release);
948 }
949}
950
951pub fn alt_screen_active() -> bool {
959 ALT_SCREEN_ACTIVE.load(Ordering::Acquire)
960}
961
962pub fn simulate_resize_signal() {
968 RESIZE_GENERATION.fetch_add(1, Ordering::Relaxed);
969 #[cfg(unix)]
970 platform::notify_resize_pipe();
971}
972
973use crate::{
974 InputDecoder, InputEvent, Keymap, ProbeResults, Renderer, Size, TerminalCaps, TerminalResponse,
975 context::Appearance,
976 escape::esc,
977 graphics::negotiate,
978 paste::{PasteEvents, PasteProgress, Pasted},
979};
980
981const RESIZE_DEBOUNCE: Duration = Duration::from_millis(50);
982const APPEARANCE_DEBOUNCE: Duration = Duration::from_millis(100);
983const OSC11_QUERY: &[u8] = esc!(background_color_query).as_bytes();
984const DRAIN_IDLE: Duration = Duration::from_millis(50);
985const DRAIN_MAX: Duration = Duration::from_millis(1_000);
986const PROGRESS_KEEPALIVE: Duration = Duration::from_millis(1_000);
987const PROGRESS_CLEAR: &[u8] = esc!(progress_clear).as_bytes();
988const TITLE_PUSH: &[u8] = esc!(title_push).as_bytes();
989const TITLE_POP: &[u8] = esc!(title_pop).as_bytes();
990const INPUT_REPORTS_OFF: &[u8] = esc!(
995 !mouse_sgr,
996 !mouse_any_event,
997 !mouse_button_event,
998 !mouse_vt200,
999 !bracketed_paste,
1000 !paste_events,
1001)
1002.as_bytes();
1003const MOUSE_TRACKING_ON: &[u8] = esc!(mouse_vt200, mouse_any_event, mouse_sgr).as_bytes();
1009const MOUSE_TRACKING_OFF: &[u8] = esc!(!mouse_sgr, !mouse_any_event, !mouse_vt200).as_bytes();
1010macro_rules! emergency_restore {
1018 (main $(, $scroll:ident)*) => {
1019 emergency_restore!(@compose [viewport_bottom,] [] $($scroll),*)
1020 };
1021 (alt $(, $scroll:ident)*) => {
1022 emergency_restore!(
1023 @compose [] [!alt_screen, !app_cursor_keys, !app_keypad, kitty_keyboard_pop,]
1024 $($scroll),*
1025 )
1026 };
1027 (@compose [$($park:tt)*] [$($alt_teardown:tt)*] $($scroll:ident),*) => {
1028 esc!(
1029 progress_clear,
1030 !sync_output,
1031 margins_reset,
1032 $($park)*
1033 autowrap,
1034 !app_cursor_keys,
1035 !app_keypad,
1036 !bracketed_paste,
1037 $($scroll,)*
1038 !paste_events,
1039 kitty_keyboard_pop,
1040 !modify_other_keys,
1041 !mouse_sgr,
1042 !mouse_any_event,
1043 !mouse_vt200,
1044 $($alt_teardown)*
1045 title_pop,
1046 cursor_visible,
1047 )
1048 .as_bytes()
1049 };
1050}
1051const XTERM_SCROLL_ON_OUTPUT: u8 = 1;
1052const XTERM_SCROLL_ON_KEY_PRESS: u8 = 2;
1053const ANSI_INSERT_MODE: u8 = 1;
1054const ANSI_NEWLINE_MODE: u8 = 2;
1055const APPEARANCE_NOTIFICATIONS_MODE: u8 = 1;
1056const IN_BAND_RESIZE_MODE: u8 = 2;
1057#[cfg(any(windows, test))]
1058const UTF8_CODEPAGE: u32 = 65001;
1059
1060#[cfg(any(windows, test))]
1061trait ConsoleCodepage {
1062 fn output_codepage(&mut self) -> u32;
1063 fn set_output_codepage(&mut self, codepage: u32) -> bool;
1064}
1065
1066#[cfg(any(windows, test))]
1067fn ensure_console_utf8(console: &mut impl ConsoleCodepage) {
1068 let codepage = console.output_codepage();
1069 if codepage != 0 && codepage != UTF8_CODEPAGE {
1070 let _ = console.set_output_codepage(UTF8_CODEPAGE);
1071 }
1072}
1073
1074#[cfg(windows)]
1075struct SystemConsoleCodepage;
1076
1077#[cfg(windows)]
1078impl ConsoleCodepage for SystemConsoleCodepage {
1079 fn output_codepage(&mut self) -> u32 {
1080 unsafe { GetConsoleOutputCP() }
1082 }
1083
1084 fn set_output_codepage(&mut self, codepage: u32) -> bool {
1085 unsafe { SetConsoleOutputCP(codepage) != 0 }
1088 }
1089}
1090
1091pub fn terminal_write_all<W: io::Write>(writer: &mut W, bytes: &[u8]) -> io::Result<()> {
1092 #[cfg(windows)]
1093 ensure_console_utf8(&mut SystemConsoleCodepage);
1094 writer.write_all(bytes)
1095}
1096
1097static ACTIVE: AtomicBool = AtomicBool::new(false);
1098static ALT_SCREEN_ACTIVE: AtomicBool = AtomicBool::new(false);
1099static XTERM_SCROLL_RESTORE_MODES: AtomicU8 = AtomicU8::new(0);
1100static ANSI_MODE_RESTORE_MODES: AtomicU8 = AtomicU8::new(0);
1101static OWNED_NOTIFICATION_MODES: AtomicU8 = AtomicU8::new(0);
1102static RESIZE_GENERATION: AtomicU64 = AtomicU64::new(0);
1103static HOOKS: LazyLock<Result<(), i32>> = LazyLock::new(platform::install_handlers);
1104static PANIC_HOOK: Once = Once::new();
1105
1106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1108pub enum CursorStyle {
1109 BlinkingBlock,
1111 SteadyBlock,
1113 BlinkingUnderline,
1115 SteadyUnderline,
1117 BlinkingBar,
1119 SteadyBar,
1121}
1122
1123impl CursorStyle {
1124 const fn sequence(self) -> &'static [u8] {
1125 match self {
1126 Self::BlinkingBlock => esc!(cursor_style_blinking_block).as_bytes(),
1127 Self::SteadyBlock => esc!(cursor_style_steady_block).as_bytes(),
1128 Self::BlinkingUnderline => esc!(cursor_style_blinking_underline).as_bytes(),
1129 Self::SteadyUnderline => esc!(cursor_style_steady_underline).as_bytes(),
1130 Self::BlinkingBar => esc!(cursor_style_blinking_bar).as_bytes(),
1131 Self::SteadyBar => esc!(cursor_style_steady_bar).as_bytes(),
1132 }
1133 }
1134}
1135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1138pub enum AltScreenUse {
1139 Interactive,
1142 Resize,
1145}
1146
1147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1149pub enum Progress {
1150 Clear,
1152 Value(u8),
1154 Error(u8),
1156 Indeterminate,
1158 Paused(u8),
1160}
1161
1162#[derive(Clone, Debug, Eq, PartialEq)]
1164pub struct TerminalOptions {
1165 pub caps: Option<TerminalCaps>,
1170 pub capture_stderr: bool,
1175 pub cursor_style: Option<CursorStyle>,
1177 pub mouse: bool,
1182 probe: ProbeResults,
1183 probe_timeout: Duration,
1184}
1185
1186impl TerminalOptions {
1187 pub fn new(caps: TerminalCaps) -> Self {
1189 Self {
1190 caps: Some(caps),
1191 capture_stderr: true,
1192 cursor_style: None,
1193 mouse: false,
1194 probe: ProbeResults::default(),
1195 probe_timeout: Duration::from_millis(150),
1196 }
1197 }
1198
1199 pub fn probe_results(mut self, probe: ProbeResults) -> Self {
1202 self.probe = probe;
1203 self
1204 }
1205
1206 pub const fn probe_timeout(mut self, timeout: Duration) -> Self {
1209 self.probe_timeout = timeout;
1210 self
1211 }
1212
1213 pub const fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
1215 self.cursor_style = Some(cursor_style);
1216 self
1217 }
1218
1219 pub const fn mouse(mut self, mouse: bool) -> Self {
1224 self.mouse = mouse;
1225 self
1226 }
1227
1228 pub const fn capture_stderr(mut self, capture: bool) -> Self {
1230 self.capture_stderr = capture;
1231 self
1232 }
1233}
1234
1235impl Default for TerminalOptions {
1236 fn default() -> Self {
1237 Self {
1238 caps: None,
1239 capture_stderr: true,
1240 cursor_style: None,
1241 mouse: false,
1242 probe: ProbeResults::default(),
1243 probe_timeout: Duration::from_millis(150),
1244 }
1245 }
1246}
1247
1248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1249enum KeyboardMode {
1250 Kitty(&'static str),
1251 ModifyOtherKeys,
1252}
1253
1254impl KeyboardMode {
1255 const fn enter(self) -> &'static [u8] {
1256 match self {
1257 Self::Kitty(sequence) => sequence.as_bytes(),
1258 Self::ModifyOtherKeys => esc!(modify_other_keys).as_bytes(),
1259 }
1260 }
1261
1262 const fn leave(self) -> &'static [u8] {
1263 match self {
1264 Self::Kitty(_) => esc!(kitty_keyboard_pop).as_bytes(),
1265 Self::ModifyOtherKeys => esc!(!modify_other_keys).as_bytes(),
1266 }
1267 }
1268}
1269
1270struct ProgressWorker {
1271 state: Arc<AtomicU16>,
1272 thread: thread::JoinHandle<()>,
1273}
1274
1275pub struct Terminal {
1281 caps: TerminalCaps,
1282 tty: File,
1283 platform: platform::State,
1284 stderr: platform::StderrGuard,
1285 keyboard: KeyboardMode,
1286 cursor_style: Option<CursorStyle>,
1287 xterm_scroll_restore_modes: u8,
1288 ansi_mode_restore_modes: u8,
1289 owned_notification_modes: u8,
1290 mouse: bool,
1291 cursor_visible: Option<bool>,
1292 alt_screen: bool,
1293 alt_mouse: bool,
1294 active: bool,
1295 inside_multiplexer: bool,
1296 seen_resize: u64,
1297 pending_resize: Option<(u64, Instant)>,
1298 appearance: Option<Appearance>,
1299 appearance_callbacks: Vec<Box<dyn FnMut(Appearance) + Send>>,
1300 appearance_query_generation: Arc<AtomicU64>,
1301 in_band_size: Option<Size>,
1302 keymap: Keymap,
1303 resize_ready: bool,
1304 resize_live: bool,
1305 events: flume::Receiver<crate::pump::TerminalEvent>,
1306 resize_watch: tokio::sync::watch::Receiver<u64>,
1307 pump: crate::pump::Pump,
1308 cell_pixel_size: Option<(u16, u16)>,
1309 progress: Option<ProgressWorker>,
1310 paste_events: PasteEvents,
1311 pending_paste: Option<Pasted>,
1312}
1313
1314impl Terminal {
1315 pub fn enter(mut options: TerminalOptions) -> io::Result<Self> {
1318 ensure_restore_hooks()?;
1319 let (caps, probe) = match options.caps {
1320 Some(caps) => (caps, std::mem::take(&mut options.probe)),
1321 None => negotiate(options.probe_timeout),
1322 };
1323 #[cfg(unix)]
1324 let (mut tty, platform) = platform::prepare()?;
1325 #[cfg(windows)]
1326 let (mut tty, mut platform) = platform::prepare()?;
1327 if ACTIVE
1328 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1329 .is_err()
1330 {
1331 return Err(io::Error::new(
1332 io::ErrorKind::AlreadyExists,
1333 "another Terminal already owns the controlling terminal",
1334 ));
1335 }
1336 if let Err(error) = platform::activate_resize_pipe() {
1337 deactivate_emergency_state();
1338 return Err(error);
1339 }
1340 if let Err(error) = crate::debug::ensure_server() {
1344 deactivate_emergency_state();
1345 return Err(error);
1346 }
1347 let stderr = match platform::StderrGuard::new(options.capture_stderr) {
1348 Ok(stderr) => stderr,
1349 Err(error) => {
1350 deactivate_emergency_state();
1351 return Err(error);
1352 },
1353 };
1354 #[cfg(unix)]
1355 let raw_result = platform::enable_raw(&tty, &platform);
1356 #[cfg(windows)]
1357 let raw_result = platform::enable_raw(&tty, &mut platform);
1358 if let Err(error) = raw_result {
1359 deactivate_emergency_state();
1360 return Err(error);
1361 }
1362
1363 let keyboard = keyboard_mode(caps.kitty_keyboard);
1364 let xterm_scroll_restore_modes = xterm_scroll_restore_modes(caps);
1365 let ansi_mode_restore_modes = ansi_mode_restore_modes(&probe);
1366 let owned_notification_modes = owned_notification_modes(caps, &probe);
1367 XTERM_SCROLL_RESTORE_MODES.store(xterm_scroll_restore_modes, Ordering::Release);
1368 ANSI_MODE_RESTORE_MODES.store(ansi_mode_restore_modes, Ordering::Release);
1369 OWNED_NOTIFICATION_MODES.store(owned_notification_modes, Ordering::Release);
1370 let batch = compose_enter(
1371 keyboard,
1372 options.cursor_style,
1373 xterm_scroll_restore_modes,
1374 owned_notification_modes,
1375 options.mouse,
1376 caps.paste_events,
1377 );
1378 if let Err(error) = terminal_write_all(&mut tty, &batch).and_then(|()| tty.flush()) {
1379 emergency_restore_inner();
1380 return Err(error);
1381 }
1382
1383 let appearance = caps
1384 .background
1385 .map(|(red, green, blue)| Appearance::from_rgb16(red, green, blue));
1386 let mut decoder = InputDecoder::new();
1387 decoder.set_kitty_keyboard(matches!(keyboard, KeyboardMode::Kitty(_)));
1388 let keymap = decoder.keymap().clone();
1389 let channels = match Self::acquire_input().and_then(|input| {
1392 crate::pump::spawn(
1393 input,
1394 decoder,
1395 &probe.preserved_input,
1396 #[cfg(unix)]
1397 platform::resize_pipe_reader().ok(),
1398 #[cfg(windows)]
1399 None,
1400 )
1401 }) {
1402 Ok(channels) => channels,
1403 Err(error) => {
1404 emergency_restore_inner();
1405 return Err(error);
1406 },
1407 };
1408 channels.pump.publish();
1409 Ok(Self {
1410 caps,
1411 tty,
1412 platform,
1413 stderr,
1414 keyboard,
1415 cursor_style: options.cursor_style,
1416 xterm_scroll_restore_modes,
1417 ansi_mode_restore_modes,
1418 owned_notification_modes,
1419 mouse: options.mouse,
1420 cursor_visible: Some(false),
1421 alt_screen: false,
1422 alt_mouse: false,
1423 active: true,
1424 inside_multiplexer: caps.inside_multiplexer,
1425 seen_resize: RESIZE_GENERATION.load(Ordering::Acquire),
1426 pending_resize: None,
1427 appearance,
1428 appearance_callbacks: Vec::new(),
1429 appearance_query_generation: Arc::new(AtomicU64::new(0)),
1430 in_band_size: None,
1431 keymap,
1432 resize_ready: false,
1433 resize_live: true,
1434 events: channels.events,
1435 resize_watch: channels.resize,
1436 pump: channels.pump,
1437 cell_pixel_size: caps.cell_px,
1438 progress: None,
1439 paste_events: PasteEvents::default(),
1440 pending_paste: None,
1441 })
1442 }
1443
1444 fn acquire_input() -> io::Result<crate::pump::Input> {
1452 #[cfg(all(unix, not(target_os = "macos")))]
1453 {
1454 use std::fs::OpenOptions;
1455 Ok(crate::pump::Input::Pollable(crate::tty::open(OpenOptions::new().read(true))?))
1456 }
1457 #[cfg(target_os = "macos")]
1458 {
1459 use std::fs::OpenOptions;
1460 let stdin_is_tty = unsafe { nix::libc::isatty(nix::libc::STDIN_FILENO) } == 1;
1462 let input = if stdin_is_tty && !crate::tty::overridden() {
1463 let fd = unsafe { nix::libc::dup(nix::libc::STDIN_FILENO) };
1465 if fd < 0 {
1466 return Err(io::Error::last_os_error());
1467 }
1468 unsafe {
1470 use std::os::fd::FromRawFd as _;
1471 File::from_raw_fd(fd)
1472 }
1473 } else {
1474 crate::tty::open(OpenOptions::new().read(true))?
1475 };
1476 Ok(crate::pump::Input::Bridged(input))
1477 }
1478 #[cfg(windows)]
1479 {
1480 Ok(crate::pump::Input::Bridged(std::fs::OpenOptions::new().read(true).open("CONIN$")?))
1481 }
1482 }
1483
1484 pub fn leave(&mut self) -> io::Result<()> {
1491 if !self.active {
1492 return Ok(());
1493 }
1494 let mut first_error = None;
1495 record_error(self.stderr.restore(), &mut first_error);
1498 self
1499 .appearance_query_generation
1500 .fetch_add(1, Ordering::AcqRel);
1501
1502 if self.alt_screen {
1503 record_error(self.leave_alt(), &mut first_error);
1504 }
1505 record_error(terminal_write_all(&mut self.tty, self.keyboard.leave()), &mut first_error);
1506 let input_reports_off = compose_input_reports_off(self.owned_notification_modes);
1507 record_error(terminal_write_all(&mut self.tty, &input_reports_off), &mut first_error);
1508 record_error(self.tty.flush(), &mut first_error);
1509 record_error(self.stop_progress(false), &mut first_error);
1510 self.pump.stop();
1513 record_error(
1514 platform::drain(&self.tty, &self.platform, DRAIN_MAX, DRAIN_IDLE),
1515 &mut first_error,
1516 );
1517
1518 record_error(terminal_write_all(&mut self.tty, PROGRESS_CLEAR), &mut first_error);
1519 let tail = compose_leave(
1520 self.cursor_style.is_some(),
1521 self.xterm_scroll_restore_modes,
1522 self.ansi_mode_restore_modes,
1523 );
1524 record_error(terminal_write_all(&mut self.tty, &tail), &mut first_error);
1525 record_error(self.tty.flush(), &mut first_error);
1526 self.cursor_visible = Some(true);
1527 let raw_restored = match self.restore_raw() {
1528 Ok(()) => true,
1529 Err(error) => {
1530 record_error(Err(error), &mut first_error);
1531 false
1532 },
1533 };
1534
1535 if raw_restored {
1536 self.active = false;
1537 deactivate_emergency_state();
1538 }
1539 if let Some(error) = first_error {
1540 Err(error)
1541 } else {
1542 Ok(())
1543 }
1544 }
1545
1546 pub fn emergency_restore() {
1551 emergency_restore_inner();
1552 }
1553
1554 pub fn captured_stderr(&self) -> &[u8] {
1560 self.stderr.captured()
1561 }
1562
1563 pub const fn caps(&self) -> TerminalCaps {
1565 self.caps
1566 }
1567
1568 pub const fn keymap(&self) -> &Keymap {
1570 &self.keymap
1571 }
1572
1573 pub fn edit_keymap(&mut self, edit: impl FnOnce(&mut Keymap)) {
1576 edit(&mut self.keymap);
1577 self.pump.set_keymap(self.keymap.clone());
1578 }
1579
1580 pub fn size(&self) -> io::Result<Size> {
1582 platform::size(&self.tty, &self.platform)
1583 }
1584
1585 pub async fn next(&mut self) -> io::Result<crate::pump::TerminalEvent> {
1609 use crate::pump::TerminalEvent;
1610 self.stderr.drain();
1611 loop {
1612 tokio::select! {
1613 biased;
1614 changed = self.resize_watch.changed(), if self.resize_live => {
1615 match changed {
1616 Ok(()) => {
1617 self.resize_ready = true;
1618 return Ok(TerminalEvent::Resize);
1619 },
1620 Err(_) => self.resize_live = false,
1622 }
1623 },
1624 event = self.events.recv_async() => {
1625 match event {
1626 Ok(TerminalEvent::Resize) => {
1627 self.resize_ready = true;
1628 return Ok(TerminalEvent::Resize);
1629 },
1630 Ok(TerminalEvent::Closed) | Err(_) => {
1631 return Err(io::Error::new(
1632 io::ErrorKind::UnexpectedEof,
1633 "terminal input closed",
1634 ));
1635 },
1636 Ok(TerminalEvent::Debug(query)) => {
1637 if query.op == crate::pump::DebugOp::Quit {
1638 crate::debug::respond_debug_query(
1639 query.id,
1640 crate::debug::terminal_response(query.op)
1641 .expect("quit is terminal-owned"),
1642 );
1643 return Ok(TerminalEvent::Input(InputEvent::Key(
1644 crate::Key::Ctrl('c'),
1645 )));
1646 }
1647 match crate::debug::terminal_response(query.op) {
1648 Some(response) => {
1649 crate::debug::respond_debug_query(query.id, response);
1650 },
1651 None => return Ok(TerminalEvent::Debug(query)),
1652 }
1653 },
1654 Ok(event) => return Ok(event),
1655 }
1656 },
1657 }
1658 }
1659 }
1660
1661 pub fn take_resize(&mut self) -> io::Result<Option<Size>> {
1666 if !self.resize_ready && !self.size_changed() {
1667 return Ok(None);
1668 }
1669 self.resize_ready = false;
1670 match self.size() {
1671 Ok(size) => Ok(Some(size)),
1672 Err(error) => self.in_band_size.map(Some).ok_or(error),
1673 }
1674 }
1675
1676 pub fn sync_renderer<W: io::Write>(&self, renderer: &mut Renderer<W>) -> io::Result<()> {
1678 if let Some((width, height)) = self.cell_pixel_size {
1679 renderer.set_cell_pixel_size(width, height)?;
1680 }
1681 Ok(())
1682 }
1683
1684 pub const fn cell_pixel_size(&self) -> Option<(u16, u16)> {
1686 self.cell_pixel_size
1687 }
1688
1689 pub fn size_changed(&mut self) -> bool {
1696 let generation = RESIZE_GENERATION.load(Ordering::Acquire);
1697 if !self.inside_multiplexer {
1698 if generation == self.seen_resize {
1699 return false;
1700 }
1701 self.seen_resize = generation;
1702 self.cursor_visible = None;
1703 return true;
1704 }
1705
1706 let now = Instant::now();
1707 if generation != self.seen_resize {
1708 match self.pending_resize {
1709 Some((pending, since)) if pending == generation => {
1710 if now.duration_since(since) >= RESIZE_DEBOUNCE {
1711 self.seen_resize = generation;
1712 self.pending_resize = None;
1713 self.cursor_visible = None;
1714 return true;
1715 }
1716 },
1717 _ => self.pending_resize = Some((generation, now)),
1718 }
1719 }
1720 false
1721 }
1722
1723 pub const fn appearance(&self) -> Option<Appearance> {
1725 self.appearance
1726 }
1727
1728 pub const fn in_band_size(&self) -> Option<Size> {
1733 self.in_band_size
1734 }
1735
1736 pub fn on_appearance_change(&mut self, mut callback: impl FnMut(Appearance) + Send + 'static) {
1741 if let Some(appearance) = self.appearance {
1742 callback(appearance);
1743 }
1744 self.appearance_callbacks.push(Box::new(callback));
1745 }
1746
1747 pub fn handle_response<W: io::Write>(
1751 &mut self,
1752 response: &TerminalResponse,
1753 renderer: &mut Renderer<W>,
1754 ) -> io::Result<bool> {
1755 let consumed = self.handle_response_state(response)?;
1756 self.sync_renderer(renderer)?;
1757 Ok(consumed)
1758 }
1759
1760 fn handle_response_state(&mut self, response: &TerminalResponse) -> io::Result<bool> {
1761 if let TerminalResponse::Osc(payload) = response {
1764 return match self.paste_events.handle_osc(payload) {
1765 PasteProgress::NotMine => Ok(false),
1766 PasteProgress::Consumed => Ok(true),
1767 PasteProgress::Reply(reply) => {
1768 terminal_write_all(&mut self.tty, reply.as_bytes())?;
1769 self.tty.flush()?;
1770 Ok(true)
1771 },
1772 PasteProgress::Done(pasted) => {
1773 self.pending_paste = Some(pasted);
1774 Ok(true)
1775 },
1776 };
1777 }
1778 match *response {
1779 TerminalResponse::OscColor { index: 11, r, g, b } => {
1780 self.set_appearance(Appearance::from_rgb16(r, g, b));
1781 Ok(true)
1782 },
1783 TerminalResponse::AppearanceChanged(_) => {
1784 self.debounce_appearance_query()?;
1785 Ok(true)
1786 },
1787 TerminalResponse::InBandResize { rows, cols, x_px, y_px } => {
1788 if rows == 0 || cols == 0 || x_px == 0 || y_px == 0 {
1789 return Ok(true);
1790 }
1791 let cell_width = rounded_cell_pixels(x_px, cols);
1792 let cell_height = rounded_cell_pixels(y_px, rows);
1793 self.cell_pixel_size = Some((cell_width, cell_height));
1794 let reported = Size::new(cols, rows);
1795 self.in_band_size = Some(reconcile_in_band_geometry(reported, self.size().ok()));
1796 self.resize_ready = true;
1797 Ok(true)
1798 },
1799 _ => Ok(false),
1800 }
1801 }
1802
1803 pub fn handle_input_event<W: io::Write>(
1808 &mut self,
1809 event: &InputEvent,
1810 renderer: &mut Renderer<W>,
1811 ) -> io::Result<bool> {
1812 let InputEvent::Response(response) = event else {
1813 return Ok(false);
1814 };
1815 self.handle_response(response, renderer)
1816 }
1817
1818 pub const fn take_paste(&mut self) -> Option<Pasted> {
1828 self.pending_paste.take()
1829 }
1830
1831 pub fn copy_to_clipboard(&mut self, text: &str) -> io::Result<()> {
1838 let encoded = base64::encode(text.as_bytes()).into_string();
1839 let mut sequence = String::with_capacity(esc!(osc, "52;c;").len() + encoded.len() + 1);
1840 sequence.push_str(esc!(osc, "52;c;"));
1841 sequence.push_str(&encoded);
1842 sequence.push('\x07');
1843 terminal_write_all(&mut self.tty, sequence.as_bytes())?;
1844 self.tty.flush()?;
1845 let text = text.to_owned();
1846 std::thread::Builder::new()
1847 .name("omp-tui-clipboard".into())
1848 .spawn(move || {
1849 let _ = crate::paste::write_clipboard_text(&text);
1850 })?;
1851 Ok(())
1852 }
1853
1854 fn set_appearance(&mut self, appearance: Appearance) {
1855 if self.appearance == Some(appearance) {
1856 return;
1857 }
1858 self.appearance = Some(appearance);
1859 for callback in &mut self.appearance_callbacks {
1860 callback(appearance);
1861 }
1862 }
1863
1864 fn debounce_appearance_query(&self) -> io::Result<()> {
1865 let generation = self
1866 .appearance_query_generation
1867 .fetch_add(1, Ordering::AcqRel)
1868 .wrapping_add(1);
1869 let observed = Arc::clone(&self.appearance_query_generation);
1870 let mut tty = self.tty.try_clone()?;
1871 thread::Builder::new()
1872 .name("omp-terminal-appearance".into())
1873 .spawn(move || {
1874 thread::sleep(APPEARANCE_DEBOUNCE);
1875 if observed.load(Ordering::Acquire) == generation && ACTIVE.load(Ordering::Acquire) {
1876 let _ = terminal_write_all(&mut tty, OSC11_QUERY).and_then(|()| tty.flush());
1877 }
1878 })?;
1879 Ok(())
1880 }
1881
1882 pub fn enter_alt(&mut self) -> io::Result<()> {
1885 if self.alt_screen {
1886 return Ok(());
1887 }
1888 let mut batch = SmallVec::<u8, 64>::new();
1889 batch.extend_from_slice(esc!(alt_screen).as_bytes());
1890 if let KeyboardMode::Kitty(sequence) = self.keyboard {
1891 batch.extend_from_slice(sequence.as_bytes());
1892 }
1893 batch.extend_from_slice(esc!(!cursor_visible, !autowrap, !origin, margins_reset).as_bytes());
1894 if !self.mouse {
1895 batch.extend_from_slice(MOUSE_TRACKING_ON);
1898 }
1899 terminal_write_all(&mut self.tty, &batch)?;
1900 self.tty.flush()?;
1901 self.alt_screen = true;
1902 self.alt_mouse = !self.mouse;
1903 ALT_SCREEN_ACTIVE.store(true, Ordering::Release);
1904 self.cursor_visible = Some(false);
1905 Ok(())
1906 }
1907
1908 pub fn leave_alt(&mut self) -> io::Result<()> {
1911 if !self.alt_screen {
1912 return Ok(());
1913 }
1914 let mut batch = SmallVec::<u8, 48>::new();
1915 if std::mem::take(&mut self.alt_mouse) {
1916 batch.extend_from_slice(MOUSE_TRACKING_OFF);
1917 }
1918 if matches!(self.keyboard, KeyboardMode::Kitty(_)) {
1919 batch.extend_from_slice(esc!(kitty_keyboard_pop).as_bytes());
1920 }
1921 batch.extend_from_slice(esc!(!alt_screen).as_bytes());
1922 terminal_write_all(&mut self.tty, &batch)?;
1923 self.tty.flush()?;
1924 self.alt_screen = false;
1925 ALT_SCREEN_ACTIVE.store(false, Ordering::Release);
1926 self.cursor_visible = None;
1927 Ok(())
1928 }
1929
1930 pub fn with_alt_screen<T>(
1933 &mut self,
1934 operation: impl FnOnce(&mut Self) -> io::Result<T>,
1935 ) -> io::Result<T> {
1936 self.enter_alt()?;
1937 let result = operation(self);
1938 let leave = self.leave_alt();
1939 match (result, leave) {
1940 (Err(error), _) => Err(error),
1941 (Ok(_), Err(error)) => Err(error),
1942 (Ok(value), Ok(())) => Ok(value),
1943 }
1944 }
1945
1946 pub fn stage_alt_enter(&mut self, purpose: AltScreenUse) -> Option<Str> {
1961 if self.alt_screen {
1962 if purpose == AltScreenUse::Interactive && !self.alt_mouse && !self.mouse {
1966 self.alt_mouse = true;
1967 return Some(Str::new_static(esc!(mouse_vt200, mouse_any_event, mouse_sgr)));
1968 }
1969 return None;
1970 }
1971 self.alt_screen = true;
1972 ALT_SCREEN_ACTIVE.store(true, Ordering::Release);
1973 self.cursor_visible = None;
1974 self.alt_mouse = purpose == AltScreenUse::Interactive && !self.mouse;
1975 let tracking = if self.alt_mouse {
1976 esc!(mouse_vt200, mouse_any_event, mouse_sgr)
1977 } else {
1978 ""
1979 };
1980 Some(match self.keyboard {
1981 KeyboardMode::Kitty(push) => fmts!("{}{}{}", esc!(alt_screen), push, tracking),
1982 KeyboardMode::ModifyOtherKeys => fmts!("{}{}", esc!(alt_screen), tracking),
1983 })
1984 }
1985
1986 pub fn stage_alt_leave(&mut self) -> Option<&'static str> {
1994 if !self.alt_screen {
1995 return None;
1996 }
1997 self.alt_screen = false;
1998 ALT_SCREEN_ACTIVE.store(false, Ordering::Release);
1999 self.cursor_visible = None;
2000 let mouse = std::mem::take(&mut self.alt_mouse);
2001 Some(match (self.keyboard, mouse) {
2002 (KeyboardMode::Kitty(_), true) => {
2003 esc!(!mouse_sgr, !mouse_any_event, !mouse_vt200, kitty_keyboard_pop, !alt_screen)
2004 },
2005 (KeyboardMode::Kitty(_), false) => esc!(kitty_keyboard_pop, !alt_screen),
2006 (KeyboardMode::ModifyOtherKeys, true) => {
2007 esc!(!mouse_sgr, !mouse_any_event, !mouse_vt200, !alt_screen)
2008 },
2009 (KeyboardMode::ModifyOtherKeys, false) => esc!(!alt_screen),
2010 })
2011 }
2012
2013 pub fn hide_cursor(&mut self) -> io::Result<()> {
2015 if self.cursor_visible == Some(false) {
2016 return Ok(());
2017 }
2018 terminal_write_all(&mut self.tty, esc!(!cursor_visible).as_bytes())?;
2019 self.tty.flush()?;
2020 self.cursor_visible = Some(false);
2021 Ok(())
2022 }
2023
2024 pub fn show_cursor(&mut self) -> io::Result<()> {
2026 if self.cursor_visible == Some(true) {
2027 return Ok(());
2028 }
2029 terminal_write_all(&mut self.tty, esc!(cursor_visible).as_bytes())?;
2030 self.tty.flush()?;
2031 self.cursor_visible = Some(true);
2032 Ok(())
2033 }
2034
2035 pub fn set_title(&mut self, title: &str) -> io::Result<()> {
2042 let sequence = compose_title(title);
2043 terminal_write_all(&mut self.tty, &sequence)?;
2044 self.tty.flush()
2045 }
2046
2047 pub fn set_progress(&mut self, progress: Progress) -> io::Result<()> {
2052 if progress == Progress::Clear {
2053 return self.stop_progress(true);
2054 }
2055 let state = progress_state(progress);
2056 if let Some(worker) = &self.progress {
2057 worker.state.store(state, Ordering::Release);
2058 let sequence = compose_progress(state);
2059 terminal_write_all(&mut self.tty, &sequence)?;
2060 return self.tty.flush();
2061 }
2062 let sequence = compose_progress(state);
2063 terminal_write_all(&mut self.tty, &sequence)?;
2064 self.tty.flush()?;
2065
2066 let state = Arc::new(AtomicU16::new(state));
2067 let worker_state = Arc::clone(&state);
2068 let mut tty = self.tty.try_clone()?;
2069 let worker = thread::Builder::new()
2070 .name("omp-terminal-progress".into())
2071 .spawn(move || {
2072 loop {
2073 thread::park_timeout(PROGRESS_KEEPALIVE);
2074 let current = worker_state.load(Ordering::Acquire);
2075 if current == 0 || !ACTIVE.load(Ordering::Acquire) {
2076 break;
2077 }
2078 let sequence = compose_progress(current);
2079 let _ = terminal_write_all(&mut tty, &sequence).and_then(|()| tty.flush());
2080 }
2081 })?;
2082 self.progress = Some(ProgressWorker { state, thread: worker });
2083 Ok(())
2084 }
2085
2086 fn stop_progress(&mut self, emit_clear: bool) -> io::Result<()> {
2087 if let Some(worker) = self.progress.take() {
2088 worker.state.store(0, Ordering::Release);
2089 worker.thread.thread().unpark();
2090 let _ = worker.thread.join();
2091 }
2092 if emit_clear {
2093 terminal_write_all(&mut self.tty, PROGRESS_CLEAR)?;
2094 self.tty.flush()?;
2095 }
2096 Ok(())
2097 }
2098
2099 fn restore_raw(&mut self) -> io::Result<()> {
2100 platform::restore_raw(&self.tty, &mut self.platform)
2101 }
2102}
2103fn rounded_cell_pixels(pixels: u16, cells: u16) -> u16 {
2104 let rounded = (u32::from(pixels) + u32::from(cells) / 2) / u32::from(cells);
2105 u16::try_from(rounded.max(1)).unwrap_or(u16::MAX)
2106}
2107
2108fn reconcile_in_band_geometry(reported: Size, os: Option<Size>) -> Size {
2109 match os {
2110 Some(os) if os != reported => os,
2111 _ => reported,
2112 }
2113}
2114
2115impl Drop for Terminal {
2116 fn drop(&mut self) {
2117 if self.leave().is_err() {
2118 emergency_restore_inner();
2119 }
2120 }
2121}
2122
2123const fn keyboard_mode(reported: Option<u8>) -> KeyboardMode {
2124 match reported {
2125 Some(flags) if flags & 0b0000_0001 != 0 => {
2126 if flags & 0b0000_0010 != 0 {
2127 KeyboardMode::Kitty(esc!(csi, ">3u"))
2128 } else {
2129 KeyboardMode::Kitty(esc!(csi, ">1u"))
2130 }
2131 },
2132 Some(flags) if flags & 0b0000_0010 != 0 => KeyboardMode::Kitty(esc!(csi, ">7u")),
2133 Some(_) => KeyboardMode::Kitty(esc!(csi, ">5u")),
2134 None => KeyboardMode::ModifyOtherKeys,
2135 }
2136}
2137
2138fn xterm_scroll_restore_modes(caps: TerminalCaps) -> u8 {
2139 (u8::from(caps.xterm_scroll_to_bottom_on_output) * XTERM_SCROLL_ON_OUTPUT)
2140 | (u8::from(caps.xterm_scroll_to_bottom_on_key_press) * XTERM_SCROLL_ON_KEY_PRESS)
2141}
2142
2143fn ansi_mode_restore_modes(probe: &ProbeResults) -> u8 {
2144 (u8::from(probe.insert_mode_set) * ANSI_INSERT_MODE)
2145 | (u8::from(probe.newline_mode_set) * ANSI_NEWLINE_MODE)
2146}
2147
2148fn owned_notification_modes(caps: TerminalCaps, probe: &ProbeResults) -> u8 {
2149 (u8::from(caps.appearance_notifications && !probe.appearance_notifications_set)
2150 * APPEARANCE_NOTIFICATIONS_MODE)
2151 | (u8::from(caps.in_band_resize && !probe.in_band_resize_set) * IN_BAND_RESIZE_MODE)
2152}
2153
2154fn compose_input_reports_off(owned_notification_modes: u8) -> SmallVec<u8, 96> {
2155 let mut batch = SmallVec::new();
2156 batch.extend_from_slice(INPUT_REPORTS_OFF);
2157 if owned_notification_modes & APPEARANCE_NOTIFICATIONS_MODE != 0 {
2158 batch.extend_from_slice(esc!(!appearance_notifications).as_bytes());
2159 }
2160 if owned_notification_modes & IN_BAND_RESIZE_MODE != 0 {
2161 batch.extend_from_slice(esc!(!in_band_resize).as_bytes());
2162 }
2163 batch
2164}
2165
2166fn compose_enter(
2167 keyboard: KeyboardMode,
2168 cursor_style: Option<CursorStyle>,
2169 xterm_scroll_restore_modes: u8,
2170 owned_notification_modes: u8,
2171 mouse: bool,
2172 paste_events: bool,
2173) -> SmallVec<u8, 160> {
2174 let mut batch = SmallVec::new();
2175 batch.extend_from_slice(TITLE_PUSH);
2176 batch.extend_from_slice(esc!(!insert_mode, !newline_mode).as_bytes());
2177 batch.extend_from_slice(esc!(!cursor_visible).as_bytes());
2178 if xterm_scroll_restore_modes & XTERM_SCROLL_ON_OUTPUT != 0 {
2179 batch.extend_from_slice(esc!(!scroll_on_output).as_bytes());
2180 }
2181 if xterm_scroll_restore_modes & XTERM_SCROLL_ON_KEY_PRESS != 0 {
2182 batch.extend_from_slice(esc!(!scroll_on_key_press).as_bytes());
2183 }
2184 if let Some(style) = cursor_style {
2185 batch.extend_from_slice(style.sequence());
2186 }
2187 batch.extend_from_slice(
2188 esc!(!autowrap, !origin, margins_reset, !app_cursor_keys, !app_keypad, bracketed_paste)
2189 .as_bytes(),
2190 );
2191 if owned_notification_modes & APPEARANCE_NOTIFICATIONS_MODE != 0 {
2192 batch.extend_from_slice(esc!(appearance_notifications).as_bytes());
2193 }
2194 if owned_notification_modes & IN_BAND_RESIZE_MODE != 0 {
2195 batch.extend_from_slice(esc!(in_band_resize).as_bytes());
2196 }
2197 if paste_events {
2198 batch.extend_from_slice(esc!(paste_events).as_bytes());
2199 }
2200 if mouse {
2201 batch.extend_from_slice(MOUSE_TRACKING_ON);
2202 }
2203 batch.extend_from_slice(keyboard.enter());
2204 batch
2205}
2206
2207fn compose_leave(
2208 reset_cursor_style: bool,
2209 xterm_scroll_restore_modes: u8,
2210 ansi_mode_restore_modes: u8,
2211) -> SmallVec<u8, 160> {
2212 let mut batch = SmallVec::new();
2213 batch.extend_from_slice(esc!(!sync_output).as_bytes());
2214 if xterm_scroll_restore_modes & XTERM_SCROLL_ON_OUTPUT != 0 {
2215 batch.extend_from_slice(esc!(scroll_on_output).as_bytes());
2216 }
2217 if xterm_scroll_restore_modes & XTERM_SCROLL_ON_KEY_PRESS != 0 {
2218 batch.extend_from_slice(esc!(scroll_on_key_press).as_bytes());
2219 }
2220 batch.extend_from_slice(
2221 esc!(
2222 autowrap,
2223 !app_cursor_keys,
2224 !app_keypad,
2225 style_reset,
2226 !origin,
2227 margins_reset,
2228 viewport_newline,
2229 )
2230 .as_bytes(),
2231 );
2232 if reset_cursor_style {
2233 batch.extend_from_slice(esc!(cursor_style_default).as_bytes());
2234 }
2235 batch.extend_from_slice(TITLE_POP);
2236 batch.extend_from_slice(esc!(cursor_visible).as_bytes());
2237 if ansi_mode_restore_modes & ANSI_INSERT_MODE != 0 {
2238 batch.extend_from_slice(esc!(insert_mode).as_bytes());
2239 }
2240 if ansi_mode_restore_modes & ANSI_NEWLINE_MODE != 0 {
2241 batch.extend_from_slice(esc!(newline_mode).as_bytes());
2242 }
2243 batch
2244}
2245
2246fn compose_title(title: &str) -> SmallVec<u8, 128> {
2247 let mut sequence = SmallVec::new();
2248 sequence.extend_from_slice(esc!(osc, "0;").as_bytes());
2249 for character in title.chars().filter(|character| !character.is_control()) {
2250 let mut bytes = [0; 4];
2251 sequence.extend_from_slice(character.encode_utf8(&mut bytes).as_bytes());
2252 }
2253 sequence.extend_from_slice(esc!(bel).as_bytes());
2254 sequence
2255}
2256
2257fn progress_state(progress: Progress) -> u16 {
2258 match progress {
2259 Progress::Clear => 0,
2260 Progress::Value(percent) => 0x100 | u16::from(percent.min(100)),
2261 Progress::Error(percent) => 0x200 | u16::from(percent.min(100)),
2262 Progress::Indeterminate => 0x300,
2263 Progress::Paused(percent) => 0x400 | u16::from(percent.min(100)),
2264 }
2265}
2266
2267fn compose_progress(state: u16) -> SmallVec<u8, 24> {
2268 let mut sequence = SmallVec::new();
2269 let status = state >> 8;
2270 if status == 0 {
2271 sequence.extend_from_slice(PROGRESS_CLEAR);
2272 return sequence;
2273 }
2274 sequence.extend_from_slice(esc!(osc, "9;4;").as_bytes());
2275 sequence.push(b'0' + u8::try_from(status).unwrap_or(0));
2276 if status != 3 {
2277 sequence.push(b';');
2278 push_decimal(&mut sequence, state & 0xff);
2279 }
2280 sequence.extend_from_slice(esc!(bel).as_bytes());
2281 sequence
2282}
2283
2284fn push_decimal(sequence: &mut SmallVec<u8, 24>, value: u16) {
2285 if value >= 100 {
2286 sequence.extend_from_slice(b"100");
2287 } else if value >= 10 {
2288 sequence.push(b'0' + u8::try_from(value / 10).unwrap_or(0));
2289 sequence.push(b'0' + u8::try_from(value % 10).unwrap_or(0));
2290 } else {
2291 sequence.push(b'0' + u8::try_from(value).unwrap_or(0));
2292 }
2293}
2294
2295fn record_error(result: io::Result<()>, first: &mut Option<io::Error>) {
2296 if let Err(error) = result
2297 && first.is_none()
2298 {
2299 *first = Some(error);
2300 }
2301}
2302
2303fn ensure_restore_hooks() -> io::Result<()> {
2304 let result = &*HOOKS;
2305 if let Err(code) = result {
2306 return Err(io::Error::from_raw_os_error(*code));
2307 }
2308 PANIC_HOOK.call_once(|| {
2309 let previous = panic::take_hook();
2310 panic::set_hook(Box::new(move |information| {
2311 emergency_restore_inner();
2312 previous(information);
2313 }));
2314 });
2315 Ok(())
2316}
2317
2318fn emergency_restore_inner() {
2319 platform::emergency_restore_stderr();
2322 if !ACTIVE.swap(false, Ordering::AcqRel) {
2323 return;
2324 }
2325 let alt_screen = ALT_SCREEN_ACTIVE.swap(false, Ordering::AcqRel);
2326 let xterm_scroll_restore_modes = XTERM_SCROLL_RESTORE_MODES.swap(0, Ordering::AcqRel);
2327 let ansi_mode_restore_modes = ANSI_MODE_RESTORE_MODES.swap(0, Ordering::AcqRel);
2328 let owned_notification_modes = OWNED_NOTIFICATION_MODES.swap(0, Ordering::AcqRel);
2329 let payloads = [
2330 notification_modes_off_payload(owned_notification_modes),
2331 emergency_restore_payload(alt_screen, xterm_scroll_restore_modes),
2332 ansi_mode_restore_payload(ansi_mode_restore_modes),
2333 ];
2334 platform::emergency_restore(payloads);
2335}
2336
2337const fn notification_modes_off_payload(modes: u8) -> &'static [u8] {
2338 match modes & (APPEARANCE_NOTIFICATIONS_MODE | IN_BAND_RESIZE_MODE) {
2339 0 => b"",
2340 APPEARANCE_NOTIFICATIONS_MODE => esc!(!appearance_notifications).as_bytes(),
2341 IN_BAND_RESIZE_MODE => esc!(!in_band_resize).as_bytes(),
2342 _ => esc!(!appearance_notifications, !in_band_resize).as_bytes(),
2343 }
2344}
2345
2346const fn ansi_mode_restore_payload(modes: u8) -> &'static [u8] {
2347 match modes & (ANSI_INSERT_MODE | ANSI_NEWLINE_MODE) {
2348 0 => b"",
2349 ANSI_INSERT_MODE => esc!(insert_mode).as_bytes(),
2350 ANSI_NEWLINE_MODE => esc!(newline_mode).as_bytes(),
2351 _ => esc!(insert_mode, newline_mode).as_bytes(),
2352 }
2353}
2354
2355const fn emergency_restore_payload(
2356 alt_screen: bool,
2357 xterm_scroll_restore_modes: u8,
2358) -> &'static [u8] {
2359 match (alt_screen, xterm_scroll_restore_modes & 0b0000_0011) {
2360 (false, 0) => emergency_restore!(main),
2361 (false, XTERM_SCROLL_ON_OUTPUT) => emergency_restore!(main, scroll_on_output),
2362 (false, XTERM_SCROLL_ON_KEY_PRESS) => emergency_restore!(main, scroll_on_key_press),
2363 (false, _) => emergency_restore!(main, scroll_on_output, scroll_on_key_press),
2364 (true, 0) => emergency_restore!(alt),
2365 (true, XTERM_SCROLL_ON_OUTPUT) => emergency_restore!(alt, scroll_on_output),
2366 (true, XTERM_SCROLL_ON_KEY_PRESS) => emergency_restore!(alt, scroll_on_key_press),
2367 (true, _) => emergency_restore!(alt, scroll_on_output, scroll_on_key_press),
2368 }
2369}
2370
2371fn deactivate_emergency_state() {
2372 ACTIVE.store(false, Ordering::Release);
2373 ALT_SCREEN_ACTIVE.store(false, Ordering::Release);
2374 XTERM_SCROLL_RESTORE_MODES.store(0, Ordering::Release);
2375 ANSI_MODE_RESTORE_MODES.store(0, Ordering::Release);
2376 OWNED_NOTIFICATION_MODES.store(0, Ordering::Release);
2377 platform::deactivate();
2378}
2379
2380#[cfg(all(test, unix))]
2381mod tests {
2382 use std::{
2383 fs::{File, OpenOptions},
2384 mem::MaybeUninit,
2385 os::fd::AsRawFd as _,
2386 process::{Command, Output},
2387 sync::{
2388 Arc,
2389 atomic::{AtomicU64, Ordering},
2390 },
2391 thread,
2392 time::{Duration, Instant},
2393 };
2394
2395 use nix::{
2396 libc,
2397 pty::{Winsize, openpty},
2398 sys::termios::{SetArg, cfmakeraw, tcgetattr, tcsetattr},
2399 unistd::{pipe, read, write},
2400 };
2401 use parking_lot::Mutex;
2402
2403 use super::{
2404 ACTIVE, ANSI_INSERT_MODE, ANSI_NEWLINE_MODE, APPEARANCE_NOTIFICATIONS_MODE, AltScreenUse,
2405 ConsoleCodepage, CursorStyle, IN_BAND_RESIZE_MODE, INPUT_REPORTS_OFF, KeyboardMode,
2406 MOUSE_TRACKING_ON, OSC11_QUERY, Progress, RESIZE_GENERATION, TITLE_POP, TITLE_PUSH, Terminal,
2407 UTF8_CODEPAGE, XTERM_SCROLL_ON_KEY_PRESS, XTERM_SCROLL_ON_OUTPUT, ansi_mode_restore_modes,
2408 ansi_mode_restore_payload, base64, compose_enter, compose_input_reports_off, compose_leave,
2409 compose_progress, compose_title, emergency_restore_payload, ensure_console_utf8,
2410 ensure_restore_hooks, keyboard_mode, notification_modes_off_payload,
2411 owned_notification_modes, platform, progress_state, reconcile_in_band_geometry,
2412 rounded_cell_pixels,
2413 };
2414 use crate::{
2415 Appearance, InputDecoder, InputEvent, Key, Mods, Mouse, MouseButton, MouseReport,
2416 ProbeResults, Renderer, Size, TerminalResponse, escape::esc, paste::Pasted,
2417 };
2418
2419 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
2420 haystack
2421 .windows(needle.len())
2422 .any(|window| window == needle)
2423 }
2424
2425 fn osc(body: &str) -> InputEvent {
2427 InputEvent::Response(TerminalResponse::Osc(body.into()))
2428 }
2429
2430 #[tokio::test]
2431 async fn enhanced_paste_offer_replies_and_stages_the_payload() {
2432 let dir = std::env::temp_dir().join(format!("omp-tui-5522-{}", std::process::id()));
2433 std::fs::create_dir_all(&dir).expect("temp dir");
2434 let path = dir.join("tty");
2435 std::fs::write(&path, b"").expect("tty file");
2436 let tty = OpenOptions::new()
2437 .read(true)
2438 .write(true)
2439 .open(&path)
2440 .expect("tty opens");
2441 let mut terminal = test_terminal(tty);
2442 let mut renderer = Renderer::new(Vec::new());
2443 let mime = base64::encode(b"text/plain").into_string();
2444
2445 assert!(
2447 !terminal
2448 .handle_input_event(&osc("52;c;?"), &mut renderer)
2449 .expect("io ok")
2450 );
2451 for body in [
2454 "5522;type=read:status=OK:pw=123".to_owned(),
2455 format!("5522;type=read:status=DATA:mime={mime}"),
2456 "5522;type=read:status=DONE".to_owned(),
2457 ] {
2458 assert!(
2459 terminal
2460 .handle_input_event(&osc(&body), &mut renderer)
2461 .expect("io ok")
2462 );
2463 }
2464 assert!(terminal.take_paste().is_none(), "listing DONE only requests the payload");
2465 let request = std::fs::read_to_string(&path).expect("request written");
2466 assert!(request.contains("pw=123"), "read request echoes the grant: {request:?}");
2467 assert!(request.contains(&mime));
2468
2469 let chunk = base64::encode(b"hello").into_string();
2471 for body in [
2472 format!("5522;type=read:status=DATA:mime={mime};{chunk}"),
2473 "5522;type=read:status=DONE".to_owned(),
2474 ] {
2475 assert!(
2476 terminal
2477 .handle_input_event(&osc(&body), &mut renderer)
2478 .expect("io ok")
2479 );
2480 }
2481 assert_eq!(terminal.take_paste(), Some(Pasted::Text("hello".into())));
2482 std::fs::remove_dir_all(&dir).ok();
2483 }
2484
2485 #[derive(Default)]
2486 struct MockCodepage {
2487 current: u32,
2488 sets: Vec<u32>,
2489 }
2490
2491 impl ConsoleCodepage for MockCodepage {
2492 fn output_codepage(&mut self) -> u32 {
2493 self.current
2494 }
2495
2496 fn set_output_codepage(&mut self, codepage: u32) -> bool {
2497 self.sets.push(codepage);
2498 self.current = codepage;
2499 true
2500 }
2501 }
2502
2503 #[test]
2504 fn console_codepage_guard_only_reasserts_utf8_after_a_flip() {
2505 let mut utf8 = MockCodepage { current: UTF8_CODEPAGE, sets: Vec::new() };
2506 ensure_console_utf8(&mut utf8);
2507 assert_eq!(utf8.sets, [] as [u32; 0]);
2508
2509 let mut detached = MockCodepage::default();
2510 ensure_console_utf8(&mut detached);
2511 assert_eq!(detached.sets, [] as [u32; 0]);
2512
2513 let mut legacy = MockCodepage { current: 437, sets: Vec::new() };
2514 ensure_console_utf8(&mut legacy);
2515 assert_eq!(legacy.sets, [UTF8_CODEPAGE]);
2516 ensure_console_utf8(&mut legacy);
2517 assert_eq!(legacy.sets, [UTF8_CODEPAGE]);
2518 }
2519
2520 #[test]
2521 fn enter_batch_selects_reported_kitty_flags_or_xterm_fallback() {
2522 let cases = [
2523 (Some(0), esc!(csi, ">5u").as_bytes()),
2524 (Some(1), esc!(csi, ">1u").as_bytes()),
2525 (Some(2), esc!(csi, ">7u").as_bytes()),
2526 (Some(3), esc!(csi, ">3u").as_bytes()),
2527 (None, esc!(modify_other_keys).as_bytes()),
2528 ];
2529 const PREFIX: &[u8] = esc!(
2532 title_push,
2533 !insert_mode,
2534 !newline_mode,
2535 !cursor_visible,
2536 !autowrap,
2537 !origin,
2538 margins_reset,
2539 !app_cursor_keys,
2540 !app_keypad,
2541 bracketed_paste,
2542 )
2543 .as_bytes();
2544 const PREFIX_MOUSE: &[u8] = esc!(
2545 title_push,
2546 !insert_mode,
2547 !newline_mode,
2548 !cursor_visible,
2549 !autowrap,
2550 !origin,
2551 margins_reset,
2552 !app_cursor_keys,
2553 !app_keypad,
2554 bracketed_paste,
2555 mouse_vt200,
2556 mouse_any_event,
2557 mouse_sgr,
2558 )
2559 .as_bytes();
2560 for (reported, keyboard) in cases {
2561 let batch = compose_enter(keyboard_mode(reported), None, 0, 0, false, false);
2562 assert_eq!(batch.as_slice(), [PREFIX, keyboard].concat());
2563 let batch = compose_enter(keyboard_mode(reported), None, 0, 0, true, false);
2564 assert_eq!(batch.as_slice(), [PREFIX_MOUSE, keyboard].concat());
2565 }
2566 }
2567
2568 #[test]
2569 fn inherited_modes_drive_entry_ownership_and_restoration() {
2570 let mut caps = crate::detect();
2571 caps.appearance_notifications = true;
2572 caps.in_band_resize = true;
2573 let probe = ProbeResults {
2574 insert_mode_set: true,
2575 newline_mode_set: true,
2576 appearance_notifications_set: true,
2577 ..ProbeResults::default()
2578 };
2579 let ansi_modes = ansi_mode_restore_modes(&probe);
2580 let notification_modes = owned_notification_modes(caps, &probe);
2581 assert_eq!(ansi_modes, ANSI_INSERT_MODE | ANSI_NEWLINE_MODE);
2582 assert_eq!(notification_modes, IN_BAND_RESIZE_MODE);
2583
2584 let keyboard = KeyboardMode::Kitty(esc!(csi, ">5u"));
2585 let enter = compose_enter(keyboard, None, 0, notification_modes, false, false);
2586 assert!(contains(&enter, esc!(!insert_mode, !newline_mode).as_bytes()));
2587 assert!(!contains(&enter, esc!(appearance_notifications).as_bytes()));
2588 assert!(contains(&enter, esc!(in_band_resize).as_bytes()));
2589 let leave = compose_leave(false, 0, ansi_modes);
2590 assert!(
2591 leave.ends_with(esc!(title_pop, cursor_visible, insert_mode, newline_mode).as_bytes())
2592 );
2593 }
2594
2595 #[test]
2596 fn teardown_disables_owned_input_reports_before_drain_and_raw_restore() {
2597 let keyboard = KeyboardMode::Kitty(esc!(csi, ">5u")).leave();
2598 assert_eq!(keyboard, esc!(kitty_keyboard_pop).as_bytes());
2599 assert_eq!(compose_input_reports_off(0).as_slice(), INPUT_REPORTS_OFF);
2600 let reports_off =
2601 compose_input_reports_off(APPEARANCE_NOTIFICATIONS_MODE | IN_BAND_RESIZE_MODE);
2602 assert_eq!(
2603 reports_off.as_slice(),
2604 esc!(
2605 !mouse_sgr,
2606 !mouse_any_event,
2607 !mouse_button_event,
2608 !mouse_vt200,
2609 !bracketed_paste,
2610 !paste_events,
2611 !appearance_notifications,
2612 !in_band_resize,
2613 )
2614 .as_bytes()
2615 );
2616 let tail = compose_leave(true, 0, 0);
2617 assert!(tail.starts_with(esc!(!sync_output).as_bytes()));
2618 assert!(tail.ends_with(esc!(cursor_style_default, title_pop, cursor_visible).as_bytes()));
2619 }
2622
2623 #[test]
2624 fn emergency_payloads_reset_every_mode_the_tracking_set_enables() {
2625 for alt_screen in [false, true] {
2630 for modes in [
2631 0,
2632 XTERM_SCROLL_ON_OUTPUT,
2633 XTERM_SCROLL_ON_KEY_PRESS,
2634 XTERM_SCROLL_ON_OUTPUT | XTERM_SCROLL_ON_KEY_PRESS,
2635 ] {
2636 let payload = emergency_restore_payload(alt_screen, modes);
2637 for mode in String::from_utf8_lossy(MOUSE_TRACKING_ON).split('h') {
2638 if mode.is_empty() {
2639 continue;
2640 }
2641 let reset = format!("{mode}l");
2642 assert!(contains(payload, reset.as_bytes()), "missing {reset:?}");
2643 }
2644 }
2645 }
2646 }
2647
2648 #[test]
2649 fn xterm_scroll_to_bottom_modes_are_composed_in_order() {
2650 let keyboard = KeyboardMode::Kitty(esc!(csi, ">5u"));
2651 let enter_prefix = esc!(title_push, !insert_mode, !newline_mode, !cursor_visible).as_bytes();
2652 let enter_suffix = esc!(
2653 !autowrap,
2654 !origin,
2655 margins_reset,
2656 !app_cursor_keys,
2657 !app_keypad,
2658 bracketed_paste,
2659 mouse_vt200,
2660 mouse_any_event,
2661 mouse_sgr,
2662 csi,
2663 ">5u",
2664 )
2665 .as_bytes();
2666 let leave_prefix = esc!(!sync_output).as_bytes();
2667 let leave_suffix = esc!(
2668 autowrap,
2669 !app_cursor_keys,
2670 !app_keypad,
2671 style_reset,
2672 !origin,
2673 margins_reset,
2674 viewport_newline,
2675 title_pop,
2676 cursor_visible,
2677 )
2678 .as_bytes();
2679 for (modes, enter_modes, leave_modes) in [
2680 (0, esc!().as_bytes(), esc!().as_bytes()),
2681 (
2682 XTERM_SCROLL_ON_OUTPUT,
2683 esc!(!scroll_on_output).as_bytes(),
2684 esc!(scroll_on_output).as_bytes(),
2685 ),
2686 (
2687 XTERM_SCROLL_ON_KEY_PRESS,
2688 esc!(!scroll_on_key_press).as_bytes(),
2689 esc!(scroll_on_key_press).as_bytes(),
2690 ),
2691 (
2692 XTERM_SCROLL_ON_OUTPUT | XTERM_SCROLL_ON_KEY_PRESS,
2693 esc!(!scroll_on_output, !scroll_on_key_press).as_bytes(),
2694 esc!(scroll_on_output, scroll_on_key_press).as_bytes(),
2695 ),
2696 ] {
2697 assert_eq!(
2698 compose_enter(keyboard, None, modes, 0, true, false).as_slice(),
2699 [enter_prefix, enter_modes, enter_suffix].concat()
2700 );
2701 assert_eq!(
2702 compose_leave(false, modes, 0).as_slice(),
2703 [leave_prefix, leave_modes, leave_suffix].concat()
2704 );
2705 }
2706 }
2707
2708 #[test]
2709 fn emergency_payload_splices_deltas_in_wire_order() {
2710 assert_eq!(
2714 emergency_restore_payload(false, 0),
2715 esc!(
2716 progress_clear,
2717 !sync_output,
2718 margins_reset,
2719 viewport_bottom,
2720 autowrap,
2721 !app_cursor_keys,
2722 !app_keypad,
2723 !bracketed_paste,
2724 !paste_events,
2725 kitty_keyboard_pop,
2726 !modify_other_keys,
2727 !mouse_sgr,
2728 !mouse_any_event,
2729 !mouse_vt200,
2730 title_pop,
2731 cursor_visible,
2732 )
2733 .as_bytes()
2734 );
2735 assert_eq!(
2736 emergency_restore_payload(true, XTERM_SCROLL_ON_OUTPUT),
2737 esc!(
2738 progress_clear,
2739 !sync_output,
2740 margins_reset,
2741 autowrap,
2742 !app_cursor_keys,
2743 !app_keypad,
2744 !bracketed_paste,
2745 scroll_on_output,
2746 !paste_events,
2747 kitty_keyboard_pop,
2748 !modify_other_keys,
2749 !mouse_sgr,
2750 !mouse_any_event,
2751 !mouse_vt200,
2752 !alt_screen,
2753 !app_cursor_keys,
2754 !app_keypad,
2755 kitty_keyboard_pop,
2756 title_pop,
2757 cursor_visible,
2758 )
2759 .as_bytes()
2760 );
2761 }
2762
2763 #[test]
2764 fn emergency_owned_mode_deltas_are_byte_exact() {
2765 for (modes, expected) in [
2766 (0, esc!().as_bytes()),
2767 (APPEARANCE_NOTIFICATIONS_MODE, esc!(!appearance_notifications).as_bytes()),
2768 (IN_BAND_RESIZE_MODE, esc!(!in_band_resize).as_bytes()),
2769 (
2770 APPEARANCE_NOTIFICATIONS_MODE | IN_BAND_RESIZE_MODE,
2771 esc!(!appearance_notifications, !in_band_resize).as_bytes(),
2772 ),
2773 ] {
2774 assert_eq!(notification_modes_off_payload(modes), expected);
2775 }
2776 for (modes, expected) in [
2777 (0, esc!().as_bytes()),
2778 (ANSI_INSERT_MODE, esc!(insert_mode).as_bytes()),
2779 (ANSI_NEWLINE_MODE, esc!(newline_mode).as_bytes()),
2780 (ANSI_INSERT_MODE | ANSI_NEWLINE_MODE, esc!(insert_mode, newline_mode).as_bytes()),
2781 ] {
2782 assert_eq!(ansi_mode_restore_payload(modes), expected);
2783 }
2784 }
2785
2786 #[test]
2787 fn emergency_payload_selects_only_requested_scroll_mode_restores() {
2788 for alt_screen in [false, true] {
2789 for modes in [
2790 0,
2791 XTERM_SCROLL_ON_OUTPUT,
2792 XTERM_SCROLL_ON_KEY_PRESS,
2793 XTERM_SCROLL_ON_OUTPUT | XTERM_SCROLL_ON_KEY_PRESS,
2794 ] {
2795 let payload = emergency_restore_payload(alt_screen, modes);
2796 assert_eq!(
2797 contains(payload, esc!(scroll_on_output).as_bytes()),
2798 modes & XTERM_SCROLL_ON_OUTPUT != 0
2799 );
2800 assert_eq!(
2801 contains(payload, esc!(scroll_on_key_press).as_bytes()),
2802 modes & XTERM_SCROLL_ON_KEY_PRESS != 0
2803 );
2804 assert_eq!(contains(payload, esc!(!alt_screen).as_bytes()), alt_screen);
2805 }
2806 }
2807 }
2808
2809 #[test]
2810 fn stderr_guard_captures_direct_writes_and_leave_restores_fd_2() {
2811 let output = run_stderr_guard_child("leave");
2812 assert!(output.status.success(), "{output:?}");
2813 let stderr = String::from_utf8_lossy(&output.stderr);
2814 assert!(stderr.contains("stderr-after-leave"), "{stderr:?}");
2815 assert!(!stderr.contains("stderr-under-guard"), "{stderr:?}");
2816 }
2817
2818 #[test]
2819 fn panic_hook_restores_stderr_before_printing() {
2820 let output = run_stderr_guard_child("panic");
2821 assert!(!output.status.success(), "{output:?}");
2822 let stderr = String::from_utf8_lossy(&output.stderr);
2823 assert!(stderr.contains("panic-after-emergency-restore"), "{stderr:?}");
2824 }
2825
2826 #[test]
2827 fn stderr_guard_subprocess() {
2828 let runtime = tokio::runtime::Builder::new_current_thread()
2829 .enable_all()
2830 .build()
2831 .expect("test runtime builds");
2832 let _guard = runtime.enter();
2833 match std::env::var("OMP_TUI_STDERR_GUARD_CASE").as_deref() {
2834 Ok("leave") => {
2835 let before = stderr_stat();
2836 let mut terminal = test_terminal(
2837 OpenOptions::new()
2838 .read(true)
2839 .write(true)
2840 .open("/dev/null")
2841 .expect("/dev/null opens"),
2842 );
2843 terminal.stderr = platform::StderrGuard::new(true).expect("stderr capture engages");
2844 terminal.active = true;
2845 ACTIVE.store(true, Ordering::Release);
2846 raw_stderr_write(b"stderr-under-guard\n");
2847 terminal.leave().expect("terminal leaves");
2848 assert_eq!(terminal.captured_stderr(), b"stderr-under-guard\n");
2849 let after = stderr_stat();
2850 assert_eq!((after.st_dev, after.st_ino), (before.st_dev, before.st_ino));
2851 raw_stderr_write(b"stderr-after-leave\n");
2852 },
2853 Ok("panic") => {
2854 ensure_restore_hooks().expect("restore hooks install");
2855 let _guard = platform::StderrGuard::new(true).expect("stderr capture engages");
2856 ACTIVE.store(true, Ordering::Release);
2857 panic!("panic-after-emergency-restore");
2858 },
2859 _ => {},
2860 }
2861 }
2862
2863 fn run_stderr_guard_child(case: &str) -> Output {
2864 Command::new(std::env::current_exe().expect("current test executable"))
2865 .args(["--exact", "terminal::tests::stderr_guard_subprocess", "--nocapture"])
2866 .env("OMP_TUI_STDERR_GUARD_CASE", case)
2867 .output()
2868 .expect("stderr guard subprocess runs")
2869 }
2870
2871 fn stderr_stat() -> libc::stat {
2872 let mut stat = MaybeUninit::<libc::stat>::zeroed();
2873 assert_eq!(unsafe { libc::fstat(libc::STDERR_FILENO, stat.as_mut_ptr()) }, 0);
2875 unsafe { stat.assume_init() }
2877 }
2878
2879 fn raw_stderr_write(bytes: &[u8]) {
2880 assert_eq!(
2881 unsafe { libc::write(libc::STDERR_FILENO, bytes.as_ptr().cast(), bytes.len()) },
2883 bytes.len() as isize
2884 );
2885 }
2886
2887 #[test]
2888 fn title_and_progress_sequences_are_sanitized_and_exact() {
2889 assert_eq!(
2890 compose_title(esc!("omp", osc, "2;bad", bel, " title")).as_slice(),
2891 esc!(osc, "0;omp]2;bad title", bel).as_bytes()
2892 );
2893 assert!(
2894 compose_enter(KeyboardMode::Kitty(esc!(csi, ">5u")), None, 0, 0, false, false)
2895 .starts_with(TITLE_PUSH)
2896 );
2897 assert!(contains(&compose_leave(false, 0, 0), TITLE_POP));
2898 assert_eq!(
2899 compose_progress(progress_state(Progress::Value(42))).as_slice(),
2900 esc!(osc, "9;4;1;42", bel).as_bytes()
2901 );
2902 assert_eq!(
2903 compose_progress(progress_state(Progress::Error(150))).as_slice(),
2904 esc!(osc, "9;4;2;100", bel).as_bytes()
2905 );
2906 assert_eq!(
2907 compose_progress(progress_state(Progress::Indeterminate)).as_slice(),
2908 esc!(osc, "9;4;3", bel).as_bytes()
2909 );
2910 assert_eq!(
2911 compose_progress(progress_state(Progress::Paused(7))).as_slice(),
2912 esc!(osc, "9;4;4;7", bel).as_bytes()
2913 );
2914 assert_eq!(
2915 compose_progress(progress_state(Progress::Clear)).as_slice(),
2916 esc!(progress_clear).as_bytes()
2917 );
2918 }
2919
2920 #[test]
2921 fn cursor_style_is_composed_after_cursor_hide() {
2922 let batch = compose_enter(
2923 KeyboardMode::Kitty(esc!(csi, ">5u")),
2924 Some(CursorStyle::BlinkingBar),
2925 0,
2926 0,
2927 false,
2928 false,
2929 );
2930 assert!(contains(&batch, esc!(!cursor_visible, cursor_style_blinking_bar).as_bytes()));
2931 }
2932
2933 #[test]
2934 fn enter_batch_enables_enhanced_paste_only_when_supported() {
2935 let keyboard = KeyboardMode::Kitty(esc!(csi, ">5u"));
2936 let without = compose_enter(keyboard, None, 0, 0, false, false);
2937 assert!(!contains(&without, esc!(paste_events).as_bytes()));
2938 let with = compose_enter(keyboard, None, 0, 0, false, true);
2939 assert!(contains(&with, esc!(bracketed_paste, paste_events).as_bytes()));
2942 }
2943
2944 #[test]
2945 fn input_drain_stops_after_idle_window() {
2946 let (reader, writer) = pipe().expect("pipe opens");
2947 write(&writer, b"late release").expect("pipe accepts input");
2948 let started = Instant::now();
2949
2950 platform::drain_for_test(
2951 reader.as_raw_fd(),
2952 Duration::from_millis(300),
2953 Duration::from_millis(20),
2954 )
2955 .expect("drain succeeds");
2956 let elapsed = started.elapsed();
2957 assert!(elapsed >= Duration::from_millis(15));
2958 assert!(elapsed < Duration::from_millis(200));
2959 }
2960 #[test]
2961 fn resize_pipe_wakes_coalesces_and_preserves_input() {
2962 if std::env::var_os("OMP_TUI_RESIZE_PIPE_CHILD").is_none() {
2963 let output = Command::new(std::env::current_exe().expect("test executable resolves"))
2964 .args(["--exact", "terminal::tests::resize_pipe_wakes_coalesces_and_preserves_input"])
2965 .env("OMP_TUI_RESIZE_PIPE_CHILD", "1")
2966 .output()
2967 .expect("resize test child starts");
2968 assert!(output.status.success(), "{output:?}");
2969 return;
2970 }
2971 ensure_restore_hooks().expect("signal handlers install");
2972 platform::activate_resize_pipe().expect("resize pipe opens");
2973 let window = Winsize { ws_row: 24, ws_col: 80, ws_xpixel: 0, ws_ypixel: 0 };
2974 let pty = openpty(Some(&window), None).expect("PTY opens");
2975 let mut raw = tcgetattr(&pty.slave).expect("PTY attributes read");
2976 cfmakeraw(&mut raw);
2977 tcsetattr(&pty.slave, SetArg::TCSANOW, &raw).expect("PTY enters raw mode");
2978 tokio::runtime::Builder::new_current_thread()
2979 .enable_all()
2980 .build()
2981 .expect("test runtime builds")
2982 .block_on(async {
2983 let mut terminal = test_terminal(File::from(pty.slave));
2984 thread::spawn(|| {
2985 thread::sleep(Duration::from_millis(30));
2986 unsafe {
2988 libc::raise(libc::SIGWINCH);
2989 }
2990 });
2991 let started = Instant::now();
2992 let event = tokio::time::timeout(Duration::from_secs(3), terminal.next())
2993 .await
2994 .expect("resize wakes the event loop")
2995 .expect("resize event arrives");
2996 assert_eq!(event, crate::pump::TerminalEvent::Resize);
2997 assert!(started.elapsed() < Duration::from_millis(500));
2998 assert!(terminal.resize_ready);
2999 assert_eq!(terminal.take_resize().expect("resize size reads"), Some(Size::new(80, 24)));
3000
3001 unsafe {
3003 libc::raise(libc::SIGWINCH);
3004 libc::raise(libc::SIGWINCH);
3005 }
3006 let event = tokio::time::timeout(Duration::from_secs(3), terminal.next())
3007 .await
3008 .expect("burst wakes the event loop")
3009 .expect("burst resize arrives");
3010 assert_eq!(event, crate::pump::TerminalEvent::Resize);
3011 assert_eq!(
3012 terminal.take_resize().expect("coalesced resize reads"),
3013 Some(Size::new(80, 24))
3014 );
3015 while let Ok(event) =
3018 tokio::time::timeout(Duration::from_millis(30), terminal.next()).await
3019 {
3020 assert_eq!(
3021 event.expect("drained event decodes"),
3022 crate::pump::TerminalEvent::Resize,
3023 "only resize echoes remain queued"
3024 );
3025 let _ = terminal.take_resize();
3026 }
3027
3028 write(&pty.master, b"x").expect("PTY accepts key");
3029 let event = tokio::time::timeout(Duration::from_secs(1), terminal.next())
3030 .await
3031 .expect("key wakes the event loop")
3032 .expect("key decodes");
3033 assert_eq!(event, crate::pump::TerminalEvent::Input(InputEvent::Key(Key::Char('x'))));
3034 platform::deactivate();
3035 });
3036 }
3037
3038 async fn collect_inputs(
3041 terminal: &mut Terminal,
3042 renderer: &mut Renderer<Vec<u8>>,
3043 count: usize,
3044 ) -> Vec<InputEvent> {
3045 let mut events = Vec::new();
3046 while events.len() < count {
3047 let event = tokio::time::timeout(Duration::from_secs(1), terminal.next())
3048 .await
3049 .expect("event arrives")
3050 .expect("event decodes");
3051 match event {
3052 crate::pump::TerminalEvent::Input(InputEvent::Response(response)) => {
3053 terminal
3054 .handle_response(&response, renderer)
3055 .expect("response applies");
3056 },
3057 crate::pump::TerminalEvent::Input(event) => events.push(event),
3058 crate::pump::TerminalEvent::Resize => {
3059 terminal.resize_ready = false;
3061 },
3062 other => panic!("unexpected event {other:?}"),
3063 }
3064 }
3065 events
3066 }
3067
3068 #[tokio::test]
3069 async fn probe_window_events_are_first_and_responses_surface() {
3070 let (reader, _writer) = pipe().expect("pipe opens");
3071 let mut terminal = test_terminal_seeded(
3072 File::from(reader),
3073 esc!(
3074 "k",
3075 csi,
3076 "<0;4;3M",
3077 csi,
3078 "200~pasted",
3079 csi,
3080 "201~",
3081 csi,
3082 "I",
3083 osc,
3084 "11;rgb:ffff/ffff/ffff",
3085 bel,
3086 )
3087 .as_bytes(),
3088 );
3089 let mut renderer = Renderer::new(Vec::new());
3090 let events = collect_inputs(&mut terminal, &mut renderer, 4).await;
3091 assert_eq!(events, [
3092 InputEvent::Key(Key::Char('k')),
3093 InputEvent::Mouse(MouseReport {
3094 kind: Mouse::Click,
3095 col: 3,
3096 row: 2,
3097 button: MouseButton::Left,
3098 mods: Mods::default(),
3099 pressed: true,
3100 }),
3101 InputEvent::Paste("pasted".into()),
3102 InputEvent::Focus(true),
3103 ]);
3104 let event = tokio::time::timeout(Duration::from_secs(1), terminal.next())
3107 .await
3108 .expect("response arrives")
3109 .expect("response decodes");
3110 let crate::pump::TerminalEvent::Input(InputEvent::Response(response)) = event else {
3111 panic!("expected the trailing terminal response, got {event:?}");
3112 };
3113 terminal
3114 .handle_response(&response, &mut renderer)
3115 .expect("response applies");
3116 assert_eq!(terminal.appearance(), Some(Appearance::Light));
3117 }
3118 #[tokio::test]
3119 async fn probe_window_partial_sequence_continues_in_live_pump() {
3120 let (reader, writer) = pipe().expect("pipe opens");
3121 let mut terminal = test_terminal_seeded(File::from(reader), esc!(csi).as_bytes());
3122 write(&writer, b"A").expect("pipe accepts sequence tail");
3123 let mut renderer = Renderer::new(Vec::new());
3124 let events = collect_inputs(&mut terminal, &mut renderer, 1).await;
3125 assert_eq!(events, [InputEvent::Key(Key::Up)]);
3126 }
3127
3128 #[tokio::test]
3129 async fn pump_joins_split_escape_sequence_into_one_key() {
3130 let (reader, writer) = pipe().expect("pipe opens");
3131 let mut terminal = test_terminal(File::from(reader));
3132 write(&writer, esc!(escape).as_bytes()).expect("pipe accepts escape");
3133 tokio::time::sleep(Duration::from_millis(20)).await;
3136 write(&writer, b"[A").expect("pipe accepts sequence tail");
3137 let mut renderer = Renderer::new(Vec::new());
3138 let events = collect_inputs(&mut terminal, &mut renderer, 1).await;
3139 assert_eq!(events, [InputEvent::Key(Key::Up)]);
3140 }
3141
3142 #[tokio::test]
3143 async fn pump_responses_surface_and_update_terminal_state() {
3144 let (reader, writer) = pipe().expect("pipe opens");
3145 let mut terminal = test_terminal(File::from(reader));
3146 write(
3147 &writer,
3148 esc!(osc, "11;rgb:ffff/ffff/ffff", bel, csi, "48;24;80;1600;800 t").as_bytes(),
3149 )
3150 .expect("pipe accepts terminal replies");
3151 write(&writer, b"x").expect("pipe accepts trailing key");
3152 let mut renderer = Renderer::new(Vec::new());
3153 let events = collect_inputs(&mut terminal, &mut renderer, 1).await;
3156 assert_eq!(events, [InputEvent::Key(Key::Char('x'))]);
3157 assert_eq!(terminal.appearance(), Some(Appearance::Light));
3158 assert_eq!(terminal.cell_pixel_size(), Some((10, 67)));
3159 assert_eq!(terminal.take_resize().expect("resize is available"), Some(Size::new(80, 24)));
3160 }
3161
3162 #[tokio::test]
3163 async fn pump_timeout_tick_flushes_held_partial() {
3164 let (reader, writer) = pipe().expect("pipe opens");
3165 let mut terminal = test_terminal(File::from(reader));
3166 write(&writer, esc!(escape).as_bytes()).expect("pipe accepts escape");
3167 let mut renderer = Renderer::new(Vec::new());
3170 let events = collect_inputs(&mut terminal, &mut renderer, 1).await;
3171 assert_eq!(events, [InputEvent::Key(Key::Esc)]);
3172 }
3173 #[tokio::test]
3174 async fn appearance_callback_only_fires_on_a_classification_flip() {
3175 let (reader, writer) = pipe().expect("pipe opens");
3176 drop(reader);
3177 let mut terminal = test_terminal(File::from(writer));
3178 let observed = Arc::new(Mutex::new(None));
3179 let callback_observed = Arc::clone(&observed);
3180 terminal.on_appearance_change(move |appearance| *callback_observed.lock() = Some(appearance));
3181 let mut renderer = Renderer::new(Vec::new());
3182 terminal
3183 .handle_response(
3184 &TerminalResponse::OscColor {
3185 index: 11,
3186 r: u16::MAX,
3187 g: u16::MAX,
3188 b: u16::MAX,
3189 },
3190 &mut renderer,
3191 )
3192 .unwrap();
3193 assert_eq!(terminal.appearance(), Some(Appearance::Light));
3194 assert_eq!(*observed.lock(), Some(Appearance::Light));
3195 *observed.lock() = None;
3196 terminal
3197 .handle_response(
3198 &TerminalResponse::OscColor { index: 11, r: 0xeeee, g: 0xeeee, b: 0xeeee },
3199 &mut renderer,
3200 )
3201 .unwrap();
3202 assert_eq!(*observed.lock(), None);
3203 }
3204
3205 #[tokio::test]
3206 async fn appearance_pushes_collapse_to_one_debounced_query() {
3207 let (reader, writer) = pipe().expect("pipe opens");
3208 let mut terminal = test_terminal(File::from(writer));
3209 let mut renderer = Renderer::new(Vec::new());
3210 ACTIVE.store(true, Ordering::Release);
3211 terminal
3212 .handle_response(&TerminalResponse::AppearanceChanged(1), &mut renderer)
3213 .unwrap();
3214 thread::sleep(Duration::from_millis(10));
3215 terminal
3216 .handle_response(&TerminalResponse::AppearanceChanged(2), &mut renderer)
3217 .unwrap();
3218 thread::sleep(Duration::from_millis(120));
3219 let mut bytes = [0; 64];
3220 let count = read(&reader, &mut bytes).expect("query is written");
3221 ACTIVE.store(false, Ordering::Release);
3222 assert_eq!(&bytes[..count], OSC11_QUERY);
3223 }
3224
3225 #[tokio::test]
3226 async fn in_band_resize_derives_pixels_and_os_geometry_wins() {
3227 assert_eq!(rounded_cell_pixels(1000, 120), 8);
3228 assert_eq!(rounded_cell_pixels(777, 80), 10);
3229 let reported = Size::new(120, 40);
3230 let os = Size::new(100, 30);
3231 assert_eq!(reconcile_in_band_geometry(reported, Some(os)), os);
3232 assert_eq!(reconcile_in_band_geometry(reported, Some(reported)), reported);
3233
3234 let (reader, writer) = pipe().expect("pipe opens");
3235 drop(reader);
3236 let mut terminal = test_terminal(File::from(writer));
3237 let mut renderer = Renderer::new(Vec::new());
3238 terminal
3239 .handle_response(
3240 &TerminalResponse::InBandResize { rows: 40, cols: 120, x_px: 1000, y_px: 800 },
3241 &mut renderer,
3242 )
3243 .unwrap();
3244 assert_eq!(terminal.in_band_size(), Some(reported));
3245 }
3246
3247 #[tokio::test]
3248 async fn staged_alt_sequences_split_interactive_and_resize_ownership() {
3249 let (reader, writer) = pipe().expect("pipe opens");
3250 drop(reader);
3251 let mut terminal = test_terminal(File::from(writer));
3252
3253 let enter = terminal
3255 .stage_alt_enter(AltScreenUse::Interactive)
3256 .expect("first entry stages");
3257 assert_eq!(
3258 enter.as_str(),
3259 esc!(alt_screen, csi, ">5u", mouse_vt200, mouse_any_event, mouse_sgr)
3260 );
3261 assert!(
3262 terminal
3263 .stage_alt_enter(AltScreenUse::Interactive)
3264 .is_none(),
3265 "entry while active is a no-op"
3266 );
3267 let leave = terminal.stage_alt_leave().expect("exit stages");
3268 assert_eq!(
3269 leave,
3270 esc!(!mouse_sgr, !mouse_any_event, !mouse_vt200, kitty_keyboard_pop, !alt_screen)
3271 );
3272 assert!(terminal.stage_alt_leave().is_none(), "exit on the main screen is a no-op");
3273
3274 let enter = terminal
3277 .stage_alt_enter(AltScreenUse::Resize)
3278 .expect("borrow stages");
3279 assert_eq!(enter.as_str(), esc!(alt_screen, csi, ">5u"));
3280 let leave = terminal.stage_alt_leave().expect("borrow exit stages");
3281 assert_eq!(leave, esc!(kitty_keyboard_pop, !alt_screen));
3282
3283 let _ = terminal
3287 .stage_alt_enter(AltScreenUse::Resize)
3288 .expect("borrow re-stages");
3289 assert!(
3290 terminal.stage_alt_enter(AltScreenUse::Resize).is_none(),
3291 "borrow while active is a no-op"
3292 );
3293 let upgrade = terminal
3294 .stage_alt_enter(AltScreenUse::Interactive)
3295 .expect("mid-drag hold upgrades in place");
3296 assert_eq!(upgrade.as_str(), esc!(mouse_vt200, mouse_any_event, mouse_sgr));
3297 assert!(
3298 terminal
3299 .stage_alt_enter(AltScreenUse::Interactive)
3300 .is_none(),
3301 "an upgraded hold is already interactive"
3302 );
3303 let leave = terminal.stage_alt_leave().expect("upgraded exit stages");
3304 assert_eq!(
3305 leave,
3306 esc!(!mouse_sgr, !mouse_any_event, !mouse_vt200, kitty_keyboard_pop, !alt_screen)
3307 );
3308
3309 terminal.keyboard = KeyboardMode::ModifyOtherKeys;
3311 terminal.mouse = true;
3312 assert_eq!(
3313 terminal
3314 .stage_alt_enter(AltScreenUse::Interactive)
3315 .expect("re-entry stages")
3316 .as_str(),
3317 esc!(alt_screen)
3318 );
3319 assert_eq!(terminal.stage_alt_leave().expect("re-exit stages"), esc!(!alt_screen));
3320 }
3321
3322 fn test_terminal(tty: File) -> Terminal {
3325 test_terminal_seeded(tty, b"")
3326 }
3327
3328 fn test_terminal_seeded(tty: File, preserved: &[u8]) -> Terminal {
3331 let source = tty.try_clone().expect("test tty clones");
3332 let channels = match crate::pump::spawn(
3333 crate::pump::Input::Pollable(source),
3334 InputDecoder::new(),
3335 preserved,
3336 platform::resize_pipe_reader().ok(),
3337 ) {
3338 Ok(channels) => channels,
3339 Err(_) => crate::pump::spawn(
3341 crate::pump::Input::Bridged(tty.try_clone().expect("test tty clones")),
3342 InputDecoder::new(),
3343 preserved,
3344 platform::resize_pipe_reader().ok(),
3345 )
3346 .expect("bridged test actor spawns"),
3347 };
3348 Terminal {
3349 caps: crate::detect(),
3350 tty,
3351 platform: platform::state_for_test(),
3352 stderr: platform::StderrGuard::new(false).expect("disabled stderr guard"),
3353 keyboard: KeyboardMode::Kitty(esc!(csi, ">5u")),
3354 cursor_style: None,
3355 xterm_scroll_restore_modes: 0,
3356 ansi_mode_restore_modes: 0,
3357 owned_notification_modes: 0,
3358 mouse: false,
3359 cursor_visible: None,
3360 alt_screen: false,
3361 alt_mouse: false,
3362 active: false,
3363 inside_multiplexer: false,
3364 seen_resize: RESIZE_GENERATION.load(Ordering::Acquire),
3365 pending_resize: None,
3366 appearance: Some(Appearance::Dark),
3367 appearance_callbacks: Vec::new(),
3368 appearance_query_generation: Arc::new(AtomicU64::new(0)),
3369 in_band_size: None,
3370 keymap: crate::Keymap::default(),
3371 resize_ready: false,
3372 resize_live: true,
3373 events: channels.events,
3374 resize_watch: channels.resize,
3375 pump: channels.pump,
3376 cell_pixel_size: None,
3377 progress: None,
3378 paste_events: crate::paste::PasteEvents::default(),
3379 pending_paste: None,
3380 }
3381 }
3382}