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::follow::{Member, Start};
25use super::graphics_probe::LateAnswer;
26use super::handoff::{self, HandoffOutcome, HandoffScreen};
27use super::present::{Screen, pointer_shapes_supported};
28use super::signals::Signals;
29use super::terminal_clipboard::TerminalClipboard;
30use super::termination::Termination;
31use crate::env::{AssetDirs, Env};
32use crate::event::{Event, KeyEvent, KeyKind, MouseButton, MouseEvent, MouseKind};
33use crate::keymap::{Key, KeyChord, Modifiers};
34use crate::storage::{Ecosystem, Preferences, Settings};
35
36const IDLE_WAIT: Duration = Duration::from_millis(500);
38const TASK_WAIT: Duration = Duration::from_millis(20);
40
41pub struct Runtime<A: App> {
43 app: A,
44 dirs: AssetDirs,
45 theme: Option<String>,
46 settings: Option<Settings>,
47 preferences: Option<Preferences>,
48 member: Option<Member>,
49}
50
51impl<A: App> Runtime<A> {
52 pub fn new(app: A) -> Self {
54 Self { app, dirs: AssetDirs::default(), theme: None, settings: None, preferences: None, member: None }
55 }
56
57 #[must_use]
59 pub fn theme_dir(mut self, dir: impl Into<PathBuf>) -> Self {
60 self.dirs.themes = Some(dir.into());
61 self
62 }
63
64 #[must_use]
69 pub fn theme_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
70 self.dirs.theme_sources.push((file.into(), text.into()));
71 self
72 }
73
74 #[must_use]
76 pub fn icon_dir(mut self, dir: impl Into<PathBuf>) -> Self {
77 self.dirs.icons = Some(dir.into());
78 self
79 }
80
81 #[must_use]
92 pub fn icon_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
93 self.dirs.icon_sources.push((file.into(), text.into()));
94 self
95 }
96
97 #[must_use]
99 pub fn locale_dir(mut self, dir: impl Into<PathBuf>) -> Self {
100 self.dirs.locales = Some(dir.into());
101 self
102 }
103
104 #[must_use]
122 pub fn locale_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
123 self.dirs.locale_sources.push((file.into(), text.into()));
124 self
125 }
126
127 #[must_use]
129 pub fn keymap_file(mut self, file: impl Into<PathBuf>) -> Self {
130 self.dirs.keymap = Some(file.into());
131 self
132 }
133
134 #[must_use]
152 pub fn keymap_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
153 self.dirs.keymap_source = Some((file.into(), text.into()));
154 self
155 }
156
157 #[must_use]
159 pub fn theme(mut self, id: impl Into<String>) -> Self {
160 self.theme = Some(id.into());
161 self
162 }
163
164 #[must_use]
168 pub fn settings(mut self, settings: &Settings) -> Self {
169 self.settings = Some(settings.clone());
170 self
171 }
172
173 #[must_use]
196 pub fn preferences(mut self, preferences: &Preferences) -> Self {
197 self.preferences = Some(preferences.clone());
198 self
199 }
200
201 #[must_use]
237 pub fn member(mut self, ecosystem: Ecosystem, app: &str) -> Self {
238 self.member = Some(Member::new(ecosystem, app, None));
239 self
240 }
241
242 #[must_use]
245 pub fn member_in(mut self, ecosystem: Ecosystem, config_dir: impl Into<PathBuf>, app: &str) -> Self {
246 self.member = Some(Member::new(ecosystem, app, Some(config_dir.into())));
247 self
248 }
249
250 pub fn run(self) -> io::Result<()> {
279 let mut env = Env::load(&self.dirs)?;
280 let start = Start { theme: self.theme.as_deref(), settings: self.settings, preferences: self.preferences };
281 let follow = start.apply(&mut env, self.member, true);
282 let signals = Signals::catch()?;
285 let mut guard = TerminalGuard::enter()?;
286 let late = ask_graphics(&mut env, &signals);
289 guard.enhance_keyboard()?;
290 install_panic_hook();
291 let shapes = pointer_shapes_supported(|name| std::env::var(name).ok());
292 let screen = Screen::new(Terminal::new(CrosstermBackend::new(io::stdout()))?).pointer_shapes(shapes);
293 #[cfg(feature = "image")]
294 let screen = screen.measure_cell(cell_pixels);
295 let mut screen = screen;
296 let mut engine = Engine::new(self.app, env, TaskMode::Threads);
297 if let Some(follow) = follow {
298 engine.follow(follow);
299 }
300 let result = event_loop(&mut screen, engine, &guard, &signals, late);
301 if !guard.abandoned.get() {
302 let _ = screen.reset_pointer_shape();
305 #[cfg(feature = "image")]
307 let _ = screen.release_pictures();
308 }
309 let terminal = screen.into_terminal();
310 if guard.abandoned.get() {
311 std::mem::forget(terminal);
313 } else {
314 drop(terminal);
315 }
316 drop(guard);
317 drop(signals);
318 result
319 }
320}
321
322#[cfg(unix)]
326fn ask_graphics(env: &mut Env, signals: &Signals) -> LateAnswer {
327 use super::graphics_probe::{PROBE_WAIT, late_from, probe};
328 use rustix::termios::isatty;
329 if !env.graphics_worth_asking() || !isatty(signals.tty()) || !isatty(io::stdout()) {
330 return LateAnswer::default();
331 }
332 match probe(signals.tty(), &mut io::stdout(), PROBE_WAIT) {
333 Ok(probe) => {
334 env.set_terminal_graphics(probe.graphics);
335 if probe.answered { LateAnswer::default() } else { late_from(Instant::now()) }
336 }
337 Err(_) => late_from(Instant::now()),
339 }
340}
341
342#[cfg(not(unix))]
345fn ask_graphics(_env: &mut Env, _signals: &Signals) -> LateAnswer {
346 LateAnswer::default()
347}
348
349fn event_loop<A: App>(
350 terminal: &mut Screen<Stdout>,
351 mut engine: Engine<A>,
352 guard: &TerminalGuard,
353 signals: &Signals,
354 mut late: LateAnswer,
355) -> io::Result<()> {
356 let start = Instant::now();
357 let mut clipboard = TerminalClipboard::default();
358 let mut gone = false;
361 loop {
362 let now = start.elapsed();
363 let heard = signals.take();
364 if heard.resized {
365 engine.dirty = true;
368 }
369 for cause in heard.causes {
370 if cause == Termination::Hangup && !gone && signals.terminal_gone() {
371 gone = true;
372 guard.abandon();
373 }
374 engine.terminate(cause, now);
375 }
376 engine.poll_tasks();
377 engine.run_queued_work();
378 engine.follow_preferences();
379 if gone {
380 refuse_handoffs(&mut engine);
381 } else {
382 run_handoffs(terminal, &mut engine, guard, signals);
383 if let Err(error) = draw(terminal, &mut engine, &mut clipboard, start) {
385 hang_up_or(error, signals, guard, &mut gone)?;
386 }
387 }
388 engine.end_when_due(start.elapsed());
389 if engine.quit {
390 return Ok(());
391 }
392 let now = start.elapsed();
393 let mut wait = match (gone, engine.deadline()) {
394 (true, _) | (false, None) => IDLE_WAIT,
396 (false, Some(deadline)) => deadline.saturating_sub(now),
397 };
398 if let Some(deadline) = engine.ending_deadline() {
399 wait = wait.min(deadline.saturating_sub(now));
400 }
401 if engine.pending_tasks > 0 || (!gone && engine.clipboard_reader.is_reading()) {
402 wait = wait.min(TASK_WAIT);
403 }
404 if let Some(deadline) = clipboard.deadline().filter(|_| !gone) {
405 wait = wait.min(deadline.saturating_sub(now));
406 }
407 let held = if gone { None } else { engine.frame_deadline(now) };
410 if let Some(at) = held {
411 wait = wait.min(at.saturating_sub(now));
412 }
413 if (engine.dirty && held.is_none() && !gone) || engine.has_queued_work() {
414 wait = Duration::ZERO;
415 }
416 if gone {
417 signals.wait(wait, false)?;
418 continue;
419 }
420 let mut input = Input { clipboard: &mut clipboard, late: &mut late };
421 match read_input(&mut engine, &mut input, signals, start, wait) {
422 Ok(true) => hang_up(signals, guard, &mut gone),
423 Ok(false) => {}
424 Err(error) => hang_up_or(error, signals, guard, &mut gone)?,
425 }
426 }
427}
428
429fn draw<A: App>(
433 terminal: &mut Screen<Stdout>,
434 engine: &mut Engine<A>,
435 clipboard: &mut TerminalClipboard,
436 start: Instant,
437) -> io::Result<()> {
438 let now = start.elapsed();
439 clipboard.update(engine, now)?;
440 engine.tick(now);
441 if engine.frame_due(now) {
442 terminal.present(|buffer| {
443 engine.render(buffer, start.elapsed());
444 engine.painted()
445 })?;
446 for text in engine.clipboard.drain(..) {
447 execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))?;
448 }
449 }
450 Ok(())
451}
452
453struct Input<'a> {
455 clipboard: &'a mut TerminalClipboard,
456 late: &'a mut LateAnswer,
457}
458
459fn read_input<A: App>(
462 engine: &mut Engine<A>,
463 input: &mut Input<'_>,
464 signals: &Signals,
465 start: Instant,
466 wait: Duration,
467) -> io::Result<bool> {
468 let Some(mut ready) = event_waiting(signals)? else {
471 return Ok(true);
472 };
473 if !ready && !wait.is_zero() {
474 let woken = signals.wait(wait, true)?;
475 if woken.hung_up {
476 return Ok(true);
477 }
478 if woken.keyboard {
479 let Some(waiting) = event_waiting(signals)? else {
480 return Ok(true);
481 };
482 ready = waiting;
483 }
484 }
485 while ready {
486 let event = ct::read()?;
487 if let ct::Event::Resize(..) = event {
488 engine.dirty = true;
489 }
490 let more = event_waiting(signals)?;
491 ready = more == Some(true);
492 for event in input.late.filter(event, ready, Instant::now()) {
493 for event in input.clipboard.filter(event, ready, engine, start.elapsed()) {
494 if let Some(event) = translate(event) {
495 engine.handle(event, start.elapsed());
496 }
497 }
498 }
499 if input.late.take_kitty() {
500 heard_kitty_late(engine);
501 }
502 if more.is_none() {
503 return Ok(true);
504 }
505 }
506 Ok(false)
507}
508
509#[cfg(feature = "image")]
512fn cell_pixels() -> Option<(u16, u16)> {
513 let size = crossterm::terminal::window_size().ok()?;
514 if size.columns == 0 || size.rows == 0 || size.width == 0 || size.height == 0 {
515 return None;
516 }
517 Some((size.width / size.columns, size.height / size.rows))
518}
519
520fn heard_kitty_late<A: App>(engine: &mut Engine<A>) {
524 engine.env.set_terminal_graphics(crate::graphics::Graphics::Kitty);
525 engine.dirty = true;
526}
527
528fn event_waiting(signals: &Signals) -> io::Result<Option<bool>> {
532 if signals.hung_up_now() {
533 return Ok(None);
534 }
535 ct::poll(Duration::ZERO).map(Some)
536}
537
538fn hang_up_or(error: io::Error, signals: &Signals, guard: &TerminalGuard, gone: &mut bool) -> io::Result<()> {
541 if !signals.terminal_gone() {
542 return Err(error);
543 }
544 hang_up(signals, guard, gone);
545 Ok(())
546}
547
548fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
551 *gone = true;
552 guard.abandon();
553 signals.hung_up();
554}
555
556fn refuse_handoffs<A: App>(engine: &mut Engine<A>) {
559 const GONE: &str = "the terminal is gone";
560 while let Some(work) = engine.take_handoff() {
561 let message = match work {
562 HandOver::Wait(handoff) => handoff.finish(HandoffOutcome::Failed(GONE.to_owned())),
563 HandOver::Detach(handoff) => handoff.finish(DetachedOutcome::Failed(GONE.to_owned()), engine.deliveries()),
564 };
565 engine.update(message);
566 }
567}
568
569fn run_handoffs<A: App>(
574 terminal: &mut Screen<Stdout>,
575 engine: &mut Engine<A>,
576 guard: &TerminalGuard,
577 signals: &Signals,
578) {
579 while let Some(work) = engine.take_handoff() {
580 let _ = terminal.reset_pointer_shape();
583 #[cfg(feature = "image")]
585 let _ = terminal.release_pictures();
586 let prompt = engine.env.i18n().translate("quvyta.handoff.pause", &[]);
587 let deliveries = engine.deliveries();
588 let message = {
589 let mut release = |notice: Option<&str>| -> io::Result<()> {
590 guard.suspend()?;
591 let mut out = io::stdout();
592 execute!(out, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
593 if let Some(text) = notice {
594 writeln!(out, "{text}")?;
595 }
596 out.flush()
597 };
598 let mut take = || -> io::Result<()> {
599 let resumed = guard.resume();
602 let area = terminal.size()?;
608 terminal.redraw_all(area)?;
609 resumed
610 };
611 let mut wait_for_key = || wait_for_key_press(&prompt, signals);
612 let mut screen = HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key };
613 signals.handoff(true);
615 let message = match work {
616 HandOver::Wait(handoff) => handoff::run(handoff, &mut screen),
617 HandOver::Detach(handoff) => detached::run(handoff, &mut screen, &deliveries),
618 };
619 signals.handoff(false);
620 message
621 };
622 engine.dirty = true;
623 engine.update(message);
624 }
625}
626
627fn wait_for_key_press(prompt: &str, signals: &Signals) -> io::Result<()> {
629 let mut out = io::stdout();
630 write!(out, "\n{prompt}")?;
631 out.flush()?;
632 enable_raw_mode()?;
634 let pressed = wait_for_key(signals);
635 disable_raw_mode()?;
636 writeln!(out)?;
637 pressed
638}
639
640fn wait_for_key(signals: &Signals) -> io::Result<()> {
643 loop {
644 if signals.pending() {
645 return Ok(());
646 }
647 match event_waiting(signals)? {
648 None => return Ok(()),
650 Some(true) => {
651 if let ct::Event::Key(key) = ct::read()?
652 && key.kind == ct::KeyEventKind::Press
653 {
654 return Ok(());
655 }
656 }
657 Some(false) => {
658 if signals.wait(IDLE_WAIT, true)?.hung_up {
659 return Ok(());
660 }
661 }
662 }
663 }
664}
665
666struct TerminalGuard {
670 keyboard_enhanced: bool,
671 abandoned: Cell<bool>,
674}
675
676impl TerminalGuard {
677 fn enter() -> io::Result<Self> {
678 enable_raw_mode()?;
679 let guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
682 execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
683 Ok(guard)
684 }
685
686 fn enhance_keyboard(&mut self) -> io::Result<()> {
691 self.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
692 self.push_keyboard_flags()
693 }
694
695 fn suspend(&self) -> io::Result<()> {
697 release(self.keyboard_enhanced)
698 }
699
700 fn resume(&self) -> io::Result<()> {
703 take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
704 }
705
706 fn abandon(&self) {
708 self.abandoned.set(true);
709 }
710
711 fn push_keyboard_flags(&self) -> io::Result<()> {
712 push_keyboard_flags(&mut io::stdout(), self.keyboard_enhanced)
713 }
714}
715
716fn take_back(out: &mut impl Write, keyboard_enhanced: bool, raw_on: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
721 let raw = raw_on();
722 let screen = execute!(out, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide);
723 let flags = push_keyboard_flags(out, keyboard_enhanced);
724 raw.and(screen).and(flags)
725}
726
727fn push_keyboard_flags(out: &mut impl Write, keyboard_enhanced: bool) -> io::Result<()> {
728 if keyboard_enhanced {
729 execute!(
730 out,
731 PushKeyboardEnhancementFlags(
732 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
733 )
734 )?;
735 }
736 Ok(())
737}
738
739impl Drop for TerminalGuard {
740 fn drop(&mut self) {
741 if !self.abandoned.get() {
742 restore(self.keyboard_enhanced);
743 }
744 }
745}
746
747fn release(keyboard_enhanced: bool) -> io::Result<()> {
749 give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
750}
751
752pub(super) fn give_back(
757 out: &mut impl Write,
758 keyboard_enhanced: bool,
759 raw_off: impl FnOnce() -> io::Result<()>,
760) -> io::Result<()> {
761 let flags = if keyboard_enhanced { execute!(out, PopKeyboardEnhancementFlags) } else { Ok(()) };
762 let screen = execute!(out, DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen, cursor::Show);
763 let raw = raw_off();
764 let flushed = out.flush();
765 flags.and(screen).and(raw).and(flushed)
766}
767
768fn restore(keyboard_enhanced: bool) {
770 let _ = release(keyboard_enhanced);
771}
772
773fn install_panic_hook() {
774 on_panic_in_this_thread(|| restore(true));
775}
776
777fn on_panic_in_this_thread(on_panic: impl Fn() + Send + Sync + 'static) {
782 let owner = std::thread::current().id();
783 let previous = std::panic::take_hook();
784 std::panic::set_hook(Box::new(move |info| {
785 if std::thread::current().id() == owner {
786 on_panic();
787 }
788 previous(info);
789 }));
790}
791
792fn translate(event: ct::Event) -> Option<Event> {
794 match event {
795 ct::Event::Key(key) => translate_key(key).map(Event::Key),
796 ct::Event::Mouse(mouse) => translate_mouse(mouse).map(Event::Mouse),
797 ct::Event::Paste(text) => Some(Event::Paste(text)),
798 ct::Event::FocusGained | ct::Event::FocusLost | ct::Event::Resize(..) => None,
799 }
800}
801
802fn modifiers(mods: ct::KeyModifiers) -> Modifiers {
803 Modifiers {
804 ctrl: mods.contains(ct::KeyModifiers::CONTROL),
805 alt: mods.contains(ct::KeyModifiers::ALT),
806 shift: mods.contains(ct::KeyModifiers::SHIFT),
807 }
808}
809
810fn translate_key(key: ct::KeyEvent) -> Option<KeyEvent> {
811 let mut mods = modifiers(key.modifiers);
812 let code = match key.code {
813 ct::KeyCode::Char(' ') => Key::Space,
814 ct::KeyCode::Char(c) if c.is_uppercase() => {
815 mods.shift = true;
816 Key::Char(c.to_lowercase().next().unwrap_or(c))
817 }
818 ct::KeyCode::Char(c) => {
819 if !c.is_alphabetic() {
820 mods.shift = false;
821 }
822 Key::Char(c)
823 }
824 ct::KeyCode::Enter => Key::Enter,
825 ct::KeyCode::Esc => Key::Esc,
826 ct::KeyCode::Tab => Key::Tab,
827 ct::KeyCode::BackTab => {
828 mods.shift = true;
829 Key::Tab
830 }
831 ct::KeyCode::Backspace => Key::Backspace,
832 ct::KeyCode::Delete => Key::Delete,
833 ct::KeyCode::Insert => Key::Insert,
834 ct::KeyCode::Home => Key::Home,
835 ct::KeyCode::End => Key::End,
836 ct::KeyCode::PageUp => Key::PageUp,
837 ct::KeyCode::PageDown => Key::PageDown,
838 ct::KeyCode::Up => Key::Up,
839 ct::KeyCode::Down => Key::Down,
840 ct::KeyCode::Left => Key::Left,
841 ct::KeyCode::Right => Key::Right,
842 ct::KeyCode::F(n) => Key::F(n),
843 ct::KeyCode::Menu => Key::Menu,
844 _ => return None,
845 };
846 let kind = match key.kind {
847 ct::KeyEventKind::Press => KeyKind::Press,
848 ct::KeyEventKind::Repeat => KeyKind::Repeat,
849 ct::KeyEventKind::Release => KeyKind::Release,
850 };
851 let text = match key.code {
852 ct::KeyCode::Char(c) if !mods.ctrl && !mods.alt => Some(c),
853 _ => None,
854 };
855 Some(KeyEvent { chord: KeyChord { key: code, mods }, kind, text })
856}
857
858fn translate_mouse(mouse: ct::MouseEvent) -> Option<MouseEvent> {
859 let button = |b: ct::MouseButton| match b {
860 ct::MouseButton::Left => MouseButton::Left,
861 ct::MouseButton::Right => MouseButton::Right,
862 ct::MouseButton::Middle => MouseButton::Middle,
863 };
864 let kind = match mouse.kind {
865 ct::MouseEventKind::Down(b) => MouseKind::Down(button(b)),
866 ct::MouseEventKind::Up(b) => MouseKind::Up(button(b)),
867 ct::MouseEventKind::Drag(b) => MouseKind::Drag(button(b)),
868 ct::MouseEventKind::Moved => MouseKind::Moved,
869 ct::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
870 ct::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
871 ct::MouseEventKind::ScrollLeft | ct::MouseEventKind::ScrollRight => return None,
872 };
873 Some(MouseEvent { kind, x: i32::from(mouse.column), y: i32::from(mouse.row), mods: modifiers(mouse.modifiers) })
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879
880 struct Told(Vec<crate::graphics::Graphics>);
882
883 impl App for Told {
884 type Msg = crate::graphics::Graphics;
885 fn update(&mut self, graphics: Self::Msg) -> crate::runtime::Command<Self::Msg> {
886 self.0.push(graphics);
887 crate::runtime::Command::none()
888 }
889 fn view(&self, _ui: &mut crate::widget::View<'_, Self::Msg>) {}
890 fn graphics(&self, graphics: crate::graphics::Graphics) -> Option<Self::Msg> {
891 Some(graphics)
892 }
893 }
894
895 #[test]
896 fn a_late_kitty_answer_turns_pictures_to_kitty_and_the_application_hears_it() {
897 use crate::graphics::Graphics;
898 let mut engine = Engine::new(Told(Vec::new()), Env::builtin(), TaskMode::Inline);
899 let area = ratatui_core::layout::Rect::new(0, 0, 10, 4);
900 let mut buffer = ratatui_core::buffer::Buffer::empty(area);
901 engine.render(&mut buffer, Duration::ZERO);
902 assert_eq!(engine.app.0, [Graphics::HalfBlock], "no answer in time");
903 engine.dirty = false;
904 heard_kitty_late(&mut engine);
905 assert!(engine.dirty, "a frame is due");
906 engine.render(&mut buffer, Duration::from_secs(1));
907 assert_eq!(engine.app.0, [Graphics::HalfBlock, Graphics::Kitty]);
908 assert_eq!(engine.env.graphics(), Graphics::Kitty);
909 }
910
911 #[test]
912 fn panics_on_other_threads_leave_the_terminal_alone() {
913 use std::sync::Arc;
914 use std::sync::atomic::{AtomicUsize, Ordering};
915 let restores = Arc::new(AtomicUsize::new(0));
916 let counter = Arc::clone(&restores);
917 on_panic_in_this_thread(move || {
918 counter.fetch_add(1, Ordering::SeqCst);
919 });
920 let _ = std::thread::spawn(|| panic!("a background task failed")).join();
922 assert_eq!(restores.load(Ordering::SeqCst), 0, "the terminal stays in application mode");
923 let _ = std::panic::catch_unwind(|| panic!("the runtime failed"));
924 assert_eq!(restores.load(Ordering::SeqCst), 1, "a panic of the runtime thread restores it");
925 }
926
927 struct Broken;
929
930 impl Write for Broken {
931 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
932 Err(io::Error::other("the terminal is gone"))
933 }
934
935 fn flush(&mut self) -> io::Result<()> {
936 Err(io::Error::other("the terminal is gone"))
937 }
938 }
939
940 #[test]
941 fn raw_mode_is_left_even_when_the_screen_cannot_be_written() {
942 let mut raw_left = false;
943 let result = give_back(&mut Broken, true, || {
944 raw_left = true;
945 Ok(())
946 });
947 assert!(raw_left, "raw mode is a terminal setting, not output, and is always left");
948 assert_eq!(result.expect_err("the failure is reported").to_string(), "the terminal is gone");
949 }
950
951 #[test]
952 fn leaving_application_mode_writes_every_step_after_one_fails() {
953 let mut out = Vec::new();
954 let result = give_back(&mut out, true, || Err(io::Error::other("no raw mode")));
955 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
956 let text = String::from_utf8(out).expect("escape codes");
957 assert!(text.contains("\x1b[?1049l"), "the alternate screen was left: {text:?}");
958 assert!(text.contains("\x1b[?25h"), "the cursor is shown again: {text:?}");
959 }
960
961 #[test]
962 fn taking_the_terminal_back_goes_on_when_raw_mode_fails() {
963 let mut out = Vec::new();
965 let result = take_back(&mut out, false, || Err(io::Error::other("no raw mode")));
966 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
967 let text = String::from_utf8(out).expect("escape codes");
968 assert!(text.contains("\x1b[?1049h"), "the alternate screen is entered again: {text:?}");
969 }
970
971 #[test]
972 fn translates_uppercase_and_backtab() {
973 let key = |code, mods| ct::KeyEvent::new(code, mods);
974 let a = translate_key(key(ct::KeyCode::Char('A'), ct::KeyModifiers::SHIFT)).expect("key");
975 assert_eq!(a.chord, "shift+a".parse().expect("chord"));
976 assert_eq!(a.text, Some('A'));
977 let question = translate_key(key(ct::KeyCode::Char('?'), ct::KeyModifiers::SHIFT)).expect("key");
978 assert_eq!(question.chord, "?".parse().expect("chord"));
979 let back = translate_key(key(ct::KeyCode::BackTab, ct::KeyModifiers::SHIFT)).expect("key");
980 assert_eq!(back.chord, "shift+tab".parse().expect("chord"));
981 let ctrl = translate_key(key(ct::KeyCode::Char('q'), ct::KeyModifiers::CONTROL)).expect("key");
982 assert_eq!(ctrl.chord, "ctrl+q".parse().expect("chord"));
983 assert_eq!(ctrl.text, None);
984 let menu = translate_key(key(ct::KeyCode::Menu, ct::KeyModifiers::NONE)).expect("key");
985 assert_eq!(menu.chord, "menu".parse().expect("chord"));
986 }
987}