1use std::cell::Cell;
4use std::io::{self, Stdout, Write};
5use std::path::PathBuf;
6use std::time::{Duration, Instant};
7
8use crossterm::clipboard::CopyToClipboard;
9use crossterm::event::{
10 self as ct, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
11 KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
12};
13use crossterm::terminal::{
14 Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
15 supports_keyboard_enhancement,
16};
17use crossterm::{cursor, execute};
18use ratatui_core::terminal::Terminal;
19use ratatui_crossterm::CrosstermBackend;
20
21use super::app::App;
22use super::detached::{self, DetachedOutcome};
23use super::engine::{Engine, HandOver, TaskMode};
24use super::graphics_probe::LateAnswer;
25use super::handoff::{self, HandoffOutcome, HandoffScreen};
26use super::present::{Screen, pointer_shapes_supported};
27use super::signals::Signals;
28use super::terminal_clipboard::TerminalClipboard;
29use super::termination::Termination;
30use crate::env::{AssetDirs, Env};
31use crate::event::{Event, KeyEvent, KeyKind, MouseButton, MouseEvent, MouseKind};
32use crate::keymap::{Key, KeyChord, Modifiers};
33use crate::storage::{Preferences, Settings};
34
35const IDLE_WAIT: Duration = Duration::from_millis(500);
37const TASK_WAIT: Duration = Duration::from_millis(20);
39
40pub struct Runtime<A: App> {
42 app: A,
43 dirs: AssetDirs,
44 theme: Option<String>,
45 settings: Option<Settings>,
46 preferences: Option<Preferences>,
47}
48
49impl<A: App> Runtime<A> {
50 pub fn new(app: A) -> Self {
52 Self { app, dirs: AssetDirs::default(), theme: None, settings: None, preferences: None }
53 }
54
55 #[must_use]
57 pub fn theme_dir(mut self, dir: impl Into<PathBuf>) -> Self {
58 self.dirs.themes = Some(dir.into());
59 self
60 }
61
62 #[must_use]
67 pub fn theme_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
68 self.dirs.theme_sources.push((file.into(), text.into()));
69 self
70 }
71
72 #[must_use]
74 pub fn icon_dir(mut self, dir: impl Into<PathBuf>) -> Self {
75 self.dirs.icons = Some(dir.into());
76 self
77 }
78
79 #[must_use]
90 pub fn icon_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
91 self.dirs.icon_sources.push((file.into(), text.into()));
92 self
93 }
94
95 #[must_use]
97 pub fn locale_dir(mut self, dir: impl Into<PathBuf>) -> Self {
98 self.dirs.locales = Some(dir.into());
99 self
100 }
101
102 #[must_use]
120 pub fn locale_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
121 self.dirs.locale_sources.push((file.into(), text.into()));
122 self
123 }
124
125 #[must_use]
127 pub fn keymap_file(mut self, file: impl Into<PathBuf>) -> Self {
128 self.dirs.keymap = Some(file.into());
129 self
130 }
131
132 #[must_use]
150 pub fn keymap_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
151 self.dirs.keymap_source = Some((file.into(), text.into()));
152 self
153 }
154
155 #[must_use]
157 pub fn theme(mut self, id: impl Into<String>) -> Self {
158 self.theme = Some(id.into());
159 self
160 }
161
162 #[must_use]
166 pub fn settings(mut self, settings: &Settings) -> Self {
167 self.settings = Some(settings.clone());
168 self
169 }
170
171 #[must_use]
194 pub fn preferences(mut self, preferences: &Preferences) -> Self {
195 self.preferences = Some(preferences.clone());
196 self
197 }
198
199 pub fn run(self) -> io::Result<()> {
228 let mut env = Env::load(&self.dirs)?;
229 if let Some(theme) = &self.theme {
230 env.set_theme(theme);
231 }
232 if let Some(settings) = &self.settings {
233 env.apply_settings(settings);
234 }
235 if let Some(preferences) = &self.preferences {
236 env.apply_preferences(preferences);
237 }
238 let signals = Signals::catch()?;
241 let mut guard = TerminalGuard::enter()?;
242 let late = ask_graphics(&mut env, &signals);
245 guard.enhance_keyboard()?;
246 install_panic_hook();
247 let shapes = pointer_shapes_supported(|name| std::env::var(name).ok());
248 let screen = Screen::new(Terminal::new(CrosstermBackend::new(io::stdout()))?).pointer_shapes(shapes);
249 #[cfg(feature = "image")]
250 let screen = screen.measure_cell(cell_pixels);
251 let mut screen = screen;
252 let engine = Engine::new(self.app, env, TaskMode::Threads);
253 let result = event_loop(&mut screen, engine, &guard, &signals, late);
254 if !guard.abandoned.get() {
255 let _ = screen.reset_pointer_shape();
258 #[cfg(feature = "image")]
260 let _ = screen.release_pictures();
261 }
262 let terminal = screen.into_terminal();
263 if guard.abandoned.get() {
264 std::mem::forget(terminal);
266 } else {
267 drop(terminal);
268 }
269 drop(guard);
270 drop(signals);
271 result
272 }
273}
274
275#[cfg(unix)]
279fn ask_graphics(env: &mut Env, signals: &Signals) -> LateAnswer {
280 use super::graphics_probe::{PROBE_WAIT, late_from, probe};
281 use rustix::termios::isatty;
282 if !env.graphics_worth_asking() || !isatty(signals.tty()) || !isatty(io::stdout()) {
283 return LateAnswer::default();
284 }
285 match probe(signals.tty(), &mut io::stdout(), PROBE_WAIT) {
286 Ok(probe) => {
287 env.set_terminal_graphics(probe.graphics);
288 if probe.answered { LateAnswer::default() } else { late_from(Instant::now()) }
289 }
290 Err(_) => late_from(Instant::now()),
292 }
293}
294
295#[cfg(not(unix))]
298fn ask_graphics(_env: &mut Env, _signals: &Signals) -> LateAnswer {
299 LateAnswer::default()
300}
301
302fn event_loop<A: App>(
303 terminal: &mut Screen<Stdout>,
304 mut engine: Engine<A>,
305 guard: &TerminalGuard,
306 signals: &Signals,
307 mut late: LateAnswer,
308) -> io::Result<()> {
309 let start = Instant::now();
310 let mut clipboard = TerminalClipboard::default();
311 let mut gone = false;
314 loop {
315 let now = start.elapsed();
316 let heard = signals.take();
317 if heard.resized {
318 engine.dirty = true;
321 }
322 for cause in heard.causes {
323 if cause == Termination::Hangup && !gone && signals.terminal_gone() {
324 gone = true;
325 guard.abandon();
326 }
327 engine.terminate(cause, now);
328 }
329 engine.poll_tasks();
330 engine.run_queued_work();
331 if gone {
332 refuse_handoffs(&mut engine);
333 } else {
334 run_handoffs(terminal, &mut engine, guard, signals);
335 if let Err(error) = draw(terminal, &mut engine, &mut clipboard, start) {
337 hang_up_or(error, signals, guard, &mut gone)?;
338 }
339 }
340 engine.end_when_due(start.elapsed());
341 if engine.quit {
342 return Ok(());
343 }
344 let now = start.elapsed();
345 let mut wait = match (gone, engine.deadline()) {
346 (true, _) | (false, None) => IDLE_WAIT,
348 (false, Some(deadline)) => deadline.saturating_sub(now),
349 };
350 if let Some(deadline) = engine.ending_deadline() {
351 wait = wait.min(deadline.saturating_sub(now));
352 }
353 if engine.pending_tasks > 0 || (!gone && engine.clipboard_reader.is_reading()) {
354 wait = wait.min(TASK_WAIT);
355 }
356 if let Some(deadline) = clipboard.deadline().filter(|_| !gone) {
357 wait = wait.min(deadline.saturating_sub(now));
358 }
359 let held = if gone { None } else { engine.frame_deadline(now) };
362 if let Some(at) = held {
363 wait = wait.min(at.saturating_sub(now));
364 }
365 if (engine.dirty && held.is_none() && !gone) || engine.has_queued_work() {
366 wait = Duration::ZERO;
367 }
368 if gone {
369 signals.wait(wait, false)?;
370 continue;
371 }
372 let mut input = Input { clipboard: &mut clipboard, late: &mut late };
373 match read_input(&mut engine, &mut input, signals, start, wait) {
374 Ok(true) => hang_up(signals, guard, &mut gone),
375 Ok(false) => {}
376 Err(error) => hang_up_or(error, signals, guard, &mut gone)?,
377 }
378 }
379}
380
381fn draw<A: App>(
385 terminal: &mut Screen<Stdout>,
386 engine: &mut Engine<A>,
387 clipboard: &mut TerminalClipboard,
388 start: Instant,
389) -> io::Result<()> {
390 let now = start.elapsed();
391 clipboard.update(engine, now)?;
392 engine.tick(now);
393 if engine.frame_due(now) {
394 terminal.present(|buffer| {
395 engine.render(buffer, start.elapsed());
396 engine.painted()
397 })?;
398 for text in engine.clipboard.drain(..) {
399 execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))?;
400 }
401 }
402 Ok(())
403}
404
405struct Input<'a> {
407 clipboard: &'a mut TerminalClipboard,
408 late: &'a mut LateAnswer,
409}
410
411fn read_input<A: App>(
414 engine: &mut Engine<A>,
415 input: &mut Input<'_>,
416 signals: &Signals,
417 start: Instant,
418 wait: Duration,
419) -> io::Result<bool> {
420 let Some(mut ready) = event_waiting(signals)? else {
423 return Ok(true);
424 };
425 if !ready && !wait.is_zero() {
426 let woken = signals.wait(wait, true)?;
427 if woken.hung_up {
428 return Ok(true);
429 }
430 if woken.keyboard {
431 let Some(waiting) = event_waiting(signals)? else {
432 return Ok(true);
433 };
434 ready = waiting;
435 }
436 }
437 while ready {
438 let event = ct::read()?;
439 if let ct::Event::Resize(..) = event {
440 engine.dirty = true;
441 }
442 let more = event_waiting(signals)?;
443 ready = more == Some(true);
444 for event in input.late.filter(event, ready, Instant::now()) {
445 for event in input.clipboard.filter(event, ready, engine, start.elapsed()) {
446 if let Some(event) = translate(event) {
447 engine.handle(event, start.elapsed());
448 }
449 }
450 }
451 if input.late.take_kitty() {
452 heard_kitty_late(engine);
453 }
454 if more.is_none() {
455 return Ok(true);
456 }
457 }
458 Ok(false)
459}
460
461#[cfg(feature = "image")]
464fn cell_pixels() -> Option<(u16, u16)> {
465 let size = crossterm::terminal::window_size().ok()?;
466 if size.columns == 0 || size.rows == 0 || size.width == 0 || size.height == 0 {
467 return None;
468 }
469 Some((size.width / size.columns, size.height / size.rows))
470}
471
472fn heard_kitty_late<A: App>(engine: &mut Engine<A>) {
476 engine.env.set_terminal_graphics(crate::graphics::Graphics::Kitty);
477 engine.dirty = true;
478}
479
480fn event_waiting(signals: &Signals) -> io::Result<Option<bool>> {
484 if signals.hung_up_now() {
485 return Ok(None);
486 }
487 ct::poll(Duration::ZERO).map(Some)
488}
489
490fn hang_up_or(error: io::Error, signals: &Signals, guard: &TerminalGuard, gone: &mut bool) -> io::Result<()> {
493 if !signals.terminal_gone() {
494 return Err(error);
495 }
496 hang_up(signals, guard, gone);
497 Ok(())
498}
499
500fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
503 *gone = true;
504 guard.abandon();
505 signals.hung_up();
506}
507
508fn refuse_handoffs<A: App>(engine: &mut Engine<A>) {
511 const GONE: &str = "the terminal is gone";
512 while let Some(work) = engine.take_handoff() {
513 let message = match work {
514 HandOver::Wait(handoff) => handoff.finish(HandoffOutcome::Failed(GONE.to_owned())),
515 HandOver::Detach(handoff) => handoff.finish(DetachedOutcome::Failed(GONE.to_owned()), engine.deliveries()),
516 };
517 engine.update(message);
518 }
519}
520
521fn run_handoffs<A: App>(
526 terminal: &mut Screen<Stdout>,
527 engine: &mut Engine<A>,
528 guard: &TerminalGuard,
529 signals: &Signals,
530) {
531 while let Some(work) = engine.take_handoff() {
532 let _ = terminal.reset_pointer_shape();
535 #[cfg(feature = "image")]
537 let _ = terminal.release_pictures();
538 let prompt = engine.env.i18n().translate("quvyta.handoff.pause", &[]);
539 let deliveries = engine.deliveries();
540 let message = {
541 let mut release = |notice: Option<&str>| -> io::Result<()> {
542 guard.suspend()?;
543 let mut out = io::stdout();
544 execute!(out, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
545 if let Some(text) = notice {
546 writeln!(out, "{text}")?;
547 }
548 out.flush()
549 };
550 let mut take = || -> io::Result<()> {
551 let resumed = guard.resume();
554 let area = terminal.size()?;
560 terminal.redraw_all(area)?;
561 resumed
562 };
563 let mut wait_for_key = || wait_for_key_press(&prompt, signals);
564 let mut screen = HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key };
565 signals.handoff(true);
567 let message = match work {
568 HandOver::Wait(handoff) => handoff::run(handoff, &mut screen),
569 HandOver::Detach(handoff) => detached::run(handoff, &mut screen, &deliveries),
570 };
571 signals.handoff(false);
572 message
573 };
574 engine.dirty = true;
575 engine.update(message);
576 }
577}
578
579fn wait_for_key_press(prompt: &str, signals: &Signals) -> io::Result<()> {
581 let mut out = io::stdout();
582 write!(out, "\n{prompt}")?;
583 out.flush()?;
584 enable_raw_mode()?;
586 let pressed = wait_for_key(signals);
587 disable_raw_mode()?;
588 writeln!(out)?;
589 pressed
590}
591
592fn wait_for_key(signals: &Signals) -> io::Result<()> {
595 loop {
596 if signals.pending() {
597 return Ok(());
598 }
599 match event_waiting(signals)? {
600 None => return Ok(()),
602 Some(true) => {
603 if let ct::Event::Key(key) = ct::read()?
604 && key.kind == ct::KeyEventKind::Press
605 {
606 return Ok(());
607 }
608 }
609 Some(false) => {
610 if signals.wait(IDLE_WAIT, true)?.hung_up {
611 return Ok(());
612 }
613 }
614 }
615 }
616}
617
618struct TerminalGuard {
622 keyboard_enhanced: bool,
623 abandoned: Cell<bool>,
626}
627
628impl TerminalGuard {
629 fn enter() -> io::Result<Self> {
630 enable_raw_mode()?;
631 let guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
634 execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
635 Ok(guard)
636 }
637
638 fn enhance_keyboard(&mut self) -> io::Result<()> {
643 self.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
644 self.push_keyboard_flags()
645 }
646
647 fn suspend(&self) -> io::Result<()> {
649 release(self.keyboard_enhanced)
650 }
651
652 fn resume(&self) -> io::Result<()> {
655 take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
656 }
657
658 fn abandon(&self) {
660 self.abandoned.set(true);
661 }
662
663 fn push_keyboard_flags(&self) -> io::Result<()> {
664 push_keyboard_flags(&mut io::stdout(), self.keyboard_enhanced)
665 }
666}
667
668fn take_back(out: &mut impl Write, keyboard_enhanced: bool, raw_on: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
673 let raw = raw_on();
674 let screen = execute!(out, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide);
675 let flags = push_keyboard_flags(out, keyboard_enhanced);
676 raw.and(screen).and(flags)
677}
678
679fn push_keyboard_flags(out: &mut impl Write, keyboard_enhanced: bool) -> io::Result<()> {
680 if keyboard_enhanced {
681 execute!(
682 out,
683 PushKeyboardEnhancementFlags(
684 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
685 )
686 )?;
687 }
688 Ok(())
689}
690
691impl Drop for TerminalGuard {
692 fn drop(&mut self) {
693 if !self.abandoned.get() {
694 restore(self.keyboard_enhanced);
695 }
696 }
697}
698
699fn release(keyboard_enhanced: bool) -> io::Result<()> {
701 give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
702}
703
704pub(super) fn give_back(
709 out: &mut impl Write,
710 keyboard_enhanced: bool,
711 raw_off: impl FnOnce() -> io::Result<()>,
712) -> io::Result<()> {
713 let flags = if keyboard_enhanced { execute!(out, PopKeyboardEnhancementFlags) } else { Ok(()) };
714 let screen = execute!(out, DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen, cursor::Show);
715 let raw = raw_off();
716 let flushed = out.flush();
717 flags.and(screen).and(raw).and(flushed)
718}
719
720fn restore(keyboard_enhanced: bool) {
722 let _ = release(keyboard_enhanced);
723}
724
725fn install_panic_hook() {
726 on_panic_in_this_thread(|| restore(true));
727}
728
729fn on_panic_in_this_thread(on_panic: impl Fn() + Send + Sync + 'static) {
734 let owner = std::thread::current().id();
735 let previous = std::panic::take_hook();
736 std::panic::set_hook(Box::new(move |info| {
737 if std::thread::current().id() == owner {
738 on_panic();
739 }
740 previous(info);
741 }));
742}
743
744fn translate(event: ct::Event) -> Option<Event> {
746 match event {
747 ct::Event::Key(key) => translate_key(key).map(Event::Key),
748 ct::Event::Mouse(mouse) => translate_mouse(mouse).map(Event::Mouse),
749 ct::Event::Paste(text) => Some(Event::Paste(text)),
750 ct::Event::FocusGained | ct::Event::FocusLost | ct::Event::Resize(..) => None,
751 }
752}
753
754fn modifiers(mods: ct::KeyModifiers) -> Modifiers {
755 Modifiers {
756 ctrl: mods.contains(ct::KeyModifiers::CONTROL),
757 alt: mods.contains(ct::KeyModifiers::ALT),
758 shift: mods.contains(ct::KeyModifiers::SHIFT),
759 }
760}
761
762fn translate_key(key: ct::KeyEvent) -> Option<KeyEvent> {
763 let mut mods = modifiers(key.modifiers);
764 let code = match key.code {
765 ct::KeyCode::Char(' ') => Key::Space,
766 ct::KeyCode::Char(c) if c.is_uppercase() => {
767 mods.shift = true;
768 Key::Char(c.to_lowercase().next().unwrap_or(c))
769 }
770 ct::KeyCode::Char(c) => {
771 if !c.is_alphabetic() {
772 mods.shift = false;
773 }
774 Key::Char(c)
775 }
776 ct::KeyCode::Enter => Key::Enter,
777 ct::KeyCode::Esc => Key::Esc,
778 ct::KeyCode::Tab => Key::Tab,
779 ct::KeyCode::BackTab => {
780 mods.shift = true;
781 Key::Tab
782 }
783 ct::KeyCode::Backspace => Key::Backspace,
784 ct::KeyCode::Delete => Key::Delete,
785 ct::KeyCode::Insert => Key::Insert,
786 ct::KeyCode::Home => Key::Home,
787 ct::KeyCode::End => Key::End,
788 ct::KeyCode::PageUp => Key::PageUp,
789 ct::KeyCode::PageDown => Key::PageDown,
790 ct::KeyCode::Up => Key::Up,
791 ct::KeyCode::Down => Key::Down,
792 ct::KeyCode::Left => Key::Left,
793 ct::KeyCode::Right => Key::Right,
794 ct::KeyCode::F(n) => Key::F(n),
795 ct::KeyCode::Menu => Key::Menu,
796 _ => return None,
797 };
798 let kind = match key.kind {
799 ct::KeyEventKind::Press => KeyKind::Press,
800 ct::KeyEventKind::Repeat => KeyKind::Repeat,
801 ct::KeyEventKind::Release => KeyKind::Release,
802 };
803 let text = match key.code {
804 ct::KeyCode::Char(c) if !mods.ctrl && !mods.alt => Some(c),
805 _ => None,
806 };
807 Some(KeyEvent { chord: KeyChord { key: code, mods }, kind, text })
808}
809
810fn translate_mouse(mouse: ct::MouseEvent) -> Option<MouseEvent> {
811 let button = |b: ct::MouseButton| match b {
812 ct::MouseButton::Left => MouseButton::Left,
813 ct::MouseButton::Right => MouseButton::Right,
814 ct::MouseButton::Middle => MouseButton::Middle,
815 };
816 let kind = match mouse.kind {
817 ct::MouseEventKind::Down(b) => MouseKind::Down(button(b)),
818 ct::MouseEventKind::Up(b) => MouseKind::Up(button(b)),
819 ct::MouseEventKind::Drag(b) => MouseKind::Drag(button(b)),
820 ct::MouseEventKind::Moved => MouseKind::Moved,
821 ct::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
822 ct::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
823 ct::MouseEventKind::ScrollLeft | ct::MouseEventKind::ScrollRight => return None,
824 };
825 Some(MouseEvent { kind, x: i32::from(mouse.column), y: i32::from(mouse.row), mods: modifiers(mouse.modifiers) })
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 struct Told(Vec<crate::graphics::Graphics>);
834
835 impl App for Told {
836 type Msg = crate::graphics::Graphics;
837 fn update(&mut self, graphics: Self::Msg) -> crate::runtime::Command<Self::Msg> {
838 self.0.push(graphics);
839 crate::runtime::Command::none()
840 }
841 fn view(&self, _ui: &mut crate::widget::View<'_, Self::Msg>) {}
842 fn graphics(&self, graphics: crate::graphics::Graphics) -> Option<Self::Msg> {
843 Some(graphics)
844 }
845 }
846
847 #[test]
848 fn a_late_kitty_answer_turns_pictures_to_kitty_and_the_application_hears_it() {
849 use crate::graphics::Graphics;
850 let mut engine = Engine::new(Told(Vec::new()), Env::builtin(), TaskMode::Inline);
851 let area = ratatui_core::layout::Rect::new(0, 0, 10, 4);
852 let mut buffer = ratatui_core::buffer::Buffer::empty(area);
853 engine.render(&mut buffer, Duration::ZERO);
854 assert_eq!(engine.app.0, [Graphics::HalfBlock], "no answer in time");
855 engine.dirty = false;
856 heard_kitty_late(&mut engine);
857 assert!(engine.dirty, "a frame is due");
858 engine.render(&mut buffer, Duration::from_secs(1));
859 assert_eq!(engine.app.0, [Graphics::HalfBlock, Graphics::Kitty]);
860 assert_eq!(engine.env.graphics(), Graphics::Kitty);
861 }
862
863 #[test]
864 fn panics_on_other_threads_leave_the_terminal_alone() {
865 use std::sync::Arc;
866 use std::sync::atomic::{AtomicUsize, Ordering};
867 let restores = Arc::new(AtomicUsize::new(0));
868 let counter = Arc::clone(&restores);
869 on_panic_in_this_thread(move || {
870 counter.fetch_add(1, Ordering::SeqCst);
871 });
872 let _ = std::thread::spawn(|| panic!("a background task failed")).join();
874 assert_eq!(restores.load(Ordering::SeqCst), 0, "the terminal stays in application mode");
875 let _ = std::panic::catch_unwind(|| panic!("the runtime failed"));
876 assert_eq!(restores.load(Ordering::SeqCst), 1, "a panic of the runtime thread restores it");
877 }
878
879 struct Broken;
881
882 impl Write for Broken {
883 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
884 Err(io::Error::other("the terminal is gone"))
885 }
886
887 fn flush(&mut self) -> io::Result<()> {
888 Err(io::Error::other("the terminal is gone"))
889 }
890 }
891
892 #[test]
893 fn raw_mode_is_left_even_when_the_screen_cannot_be_written() {
894 let mut raw_left = false;
895 let result = give_back(&mut Broken, true, || {
896 raw_left = true;
897 Ok(())
898 });
899 assert!(raw_left, "raw mode is a terminal setting, not output, and is always left");
900 assert_eq!(result.expect_err("the failure is reported").to_string(), "the terminal is gone");
901 }
902
903 #[test]
904 fn leaving_application_mode_writes_every_step_after_one_fails() {
905 let mut out = Vec::new();
906 let result = give_back(&mut out, true, || Err(io::Error::other("no raw mode")));
907 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
908 let text = String::from_utf8(out).expect("escape codes");
909 assert!(text.contains("\x1b[?1049l"), "the alternate screen was left: {text:?}");
910 assert!(text.contains("\x1b[?25h"), "the cursor is shown again: {text:?}");
911 }
912
913 #[test]
914 fn taking_the_terminal_back_goes_on_when_raw_mode_fails() {
915 let mut out = Vec::new();
917 let result = take_back(&mut out, false, || Err(io::Error::other("no raw mode")));
918 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
919 let text = String::from_utf8(out).expect("escape codes");
920 assert!(text.contains("\x1b[?1049h"), "the alternate screen is entered again: {text:?}");
921 }
922
923 #[test]
924 fn translates_uppercase_and_backtab() {
925 let key = |code, mods| ct::KeyEvent::new(code, mods);
926 let a = translate_key(key(ct::KeyCode::Char('A'), ct::KeyModifiers::SHIFT)).expect("key");
927 assert_eq!(a.chord, "shift+a".parse().expect("chord"));
928 assert_eq!(a.text, Some('A'));
929 let question = translate_key(key(ct::KeyCode::Char('?'), ct::KeyModifiers::SHIFT)).expect("key");
930 assert_eq!(question.chord, "?".parse().expect("chord"));
931 let back = translate_key(key(ct::KeyCode::BackTab, ct::KeyModifiers::SHIFT)).expect("key");
932 assert_eq!(back.chord, "shift+tab".parse().expect("chord"));
933 let ctrl = translate_key(key(ct::KeyCode::Char('q'), ct::KeyModifiers::CONTROL)).expect("key");
934 assert_eq!(ctrl.chord, "ctrl+q".parse().expect("chord"));
935 assert_eq!(ctrl.text, None);
936 let menu = translate_key(key(ct::KeyCode::Menu, ct::KeyModifiers::NONE)).expect("key");
937 assert_eq!(menu.chord, "menu".parse().expect("chord"));
938 }
939}