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::harness::Harness;
28use super::present::{Screen, pointer_shapes_supported};
29use super::signals::Signals;
30use super::terminal_clipboard::TerminalClipboard;
31use super::termination::Termination;
32use crate::env::{AssetDirs, Env};
33use crate::event::{Event, KeyEvent, KeyKind, MouseButton, MouseEvent, MouseKind};
34use crate::keymap::{Key, KeyChord, Modifiers};
35use crate::storage::{Ecosystem, Preferences, Settings};
36
37const IDLE_WAIT: Duration = Duration::from_millis(500);
39const TASK_WAIT: Duration = Duration::from_millis(20);
41
42pub struct Runtime<A: App> {
44 app: A,
45 dirs: AssetDirs,
46 theme: Option<String>,
47 settings: Option<Settings>,
48 preferences: Option<Preferences>,
49 member: Option<Member>,
50}
51
52impl<A: App> Runtime<A> {
53 pub fn new(app: A) -> Self {
55 Self { app, dirs: AssetDirs::default(), theme: None, settings: None, preferences: None, member: None }
56 }
57
58 #[must_use]
60 pub fn theme_dir(mut self, dir: impl Into<PathBuf>) -> Self {
61 self.dirs.themes = Some(dir.into());
62 self
63 }
64
65 #[must_use]
70 pub fn theme_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
71 self.dirs.theme_sources.push((file.into(), text.into()));
72 self
73 }
74
75 #[must_use]
77 pub fn icon_dir(mut self, dir: impl Into<PathBuf>) -> Self {
78 self.dirs.icons = Some(dir.into());
79 self
80 }
81
82 #[must_use]
93 pub fn icon_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
94 self.dirs.icon_sources.push((file.into(), text.into()));
95 self
96 }
97
98 #[must_use]
100 pub fn locale_dir(mut self, dir: impl Into<PathBuf>) -> Self {
101 self.dirs.locales = Some(dir.into());
102 self
103 }
104
105 #[must_use]
123 pub fn locale_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
124 self.dirs.locale_sources.push((file.into(), text.into()));
125 self
126 }
127
128 #[must_use]
130 pub fn keymap_file(mut self, file: impl Into<PathBuf>) -> Self {
131 self.dirs.keymap = Some(file.into());
132 self
133 }
134
135 #[must_use]
153 pub fn keymap_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
154 self.dirs.keymap_source = Some((file.into(), text.into()));
155 self
156 }
157
158 #[must_use]
160 pub fn theme(mut self, id: impl Into<String>) -> Self {
161 self.theme = Some(id.into());
162 self
163 }
164
165 #[must_use]
169 pub fn settings(mut self, settings: &Settings) -> Self {
170 self.settings = Some(settings.clone());
171 self
172 }
173
174 #[must_use]
197 pub fn preferences(mut self, preferences: &Preferences) -> Self {
198 self.preferences = Some(preferences.clone());
199 self
200 }
201
202 #[must_use]
238 pub fn member(mut self, ecosystem: Ecosystem, app: &str) -> Self {
239 self.member = Some(Member::new(ecosystem, app, None));
240 self
241 }
242
243 #[must_use]
246 pub fn member_in(mut self, ecosystem: Ecosystem, config_dir: impl Into<PathBuf>, app: &str) -> Self {
247 self.member = Some(Member::new(ecosystem, app, Some(config_dir.into())));
248 self
249 }
250
251 pub fn harness_in(self, config_dir: impl Into<PathBuf>, width: u16, height: u16) -> io::Result<Harness<A>> {
292 let mut env = Env::load_with(&self.dirs, |_| None)?;
293 let start = Start { theme: self.theme.as_deref(), settings: self.settings, preferences: self.preferences };
294 let member = self.member.map(|member| member.in_folder(config_dir.into()));
295 let follow = start.apply(&mut env, member, false);
296 Ok(Harness::started(self.app, env, follow, width, height))
297 }
298
299 pub fn run(self) -> io::Result<()> {
328 let mut env = Env::load(&self.dirs)?;
329 let start = Start { theme: self.theme.as_deref(), settings: self.settings, preferences: self.preferences };
330 let follow = start.apply(&mut env, self.member, true);
331 let signals = Signals::catch()?;
334 let mut guard = TerminalGuard::enter()?;
335 let late = ask_graphics(&mut env, &signals);
338 guard.enhance_keyboard()?;
339 install_panic_hook();
340 let shapes = pointer_shapes_supported(|name| std::env::var(name).ok());
341 let mut screen = Screen::new(Terminal::new(CrosstermBackend::new(io::stdout()))?).pointer_shapes(shapes);
342 env.set_cell_pixels(cell_pixels());
343 let mut engine = Engine::new(self.app, env, TaskMode::Threads);
344 if let Some(follow) = follow {
345 engine.follow(follow);
346 }
347 let result = event_loop(&mut screen, engine, &guard, &signals, late);
348 if !guard.abandoned.get() {
349 let _ = screen.reset_pointer_shape();
352 #[cfg(feature = "image")]
354 let _ = screen.release_pictures();
355 }
356 let terminal = screen.into_terminal();
357 if guard.abandoned.get() {
358 std::mem::forget(terminal);
360 } else {
361 drop(terminal);
362 }
363 drop(guard);
364 drop(signals);
365 result
366 }
367}
368
369#[cfg(unix)]
373fn ask_graphics(env: &mut Env, signals: &Signals) -> LateAnswer {
374 use super::graphics_probe::{PROBE_WAIT, late_from, probe};
375 use rustix::termios::isatty;
376 if !env.graphics_worth_asking() || !isatty(signals.tty()) || !isatty(io::stdout()) {
377 return LateAnswer::default();
378 }
379 match probe(signals.tty(), &mut io::stdout(), PROBE_WAIT) {
380 Ok(probe) => {
381 env.set_terminal_graphics(probe.graphics);
382 if probe.answered { LateAnswer::default() } else { late_from(Instant::now()) }
383 }
384 Err(_) => late_from(Instant::now()),
386 }
387}
388
389#[cfg(not(unix))]
392fn ask_graphics(_env: &mut Env, _signals: &Signals) -> LateAnswer {
393 LateAnswer::default()
394}
395
396fn event_loop<A: App>(
397 terminal: &mut Screen<Stdout>,
398 mut engine: Engine<A>,
399 guard: &TerminalGuard,
400 signals: &Signals,
401 mut late: LateAnswer,
402) -> io::Result<()> {
403 let start = Instant::now();
404 let mut clipboard = TerminalClipboard::default();
405 let mut gone = false;
408 loop {
409 let now = start.elapsed();
410 let heard = signals.take();
411 if heard.resized {
412 engine.dirty = true;
415 }
416 for cause in heard.causes {
417 if cause == Termination::Hangup && !gone && signals.terminal_gone() {
418 gone = true;
419 guard.abandon();
420 }
421 engine.terminate(cause, now);
422 }
423 engine.poll_tasks();
424 engine.run_queued_work();
425 engine.follow_preferences();
426 if gone {
427 refuse_handoffs(&mut engine);
428 } else {
429 run_handoffs(terminal, &mut engine, guard, signals);
430 if let Err(error) = draw(terminal, &mut engine, &mut clipboard, start) {
432 hang_up_or(error, signals, guard, &mut gone)?;
433 }
434 }
435 engine.end_when_due(start.elapsed());
436 if engine.quit {
437 return Ok(());
438 }
439 let now = start.elapsed();
440 let mut wait = match (gone, engine.deadline()) {
441 (true, _) | (false, None) => IDLE_WAIT,
443 (false, Some(deadline)) => deadline.saturating_sub(now),
444 };
445 if let Some(deadline) = engine.ending_deadline() {
446 wait = wait.min(deadline.saturating_sub(now));
447 }
448 if engine.pending_tasks > 0 || (!gone && engine.clipboard_reader.is_reading()) {
449 wait = wait.min(TASK_WAIT);
450 }
451 if let Some(deadline) = clipboard.deadline().filter(|_| !gone) {
452 wait = wait.min(deadline.saturating_sub(now));
453 }
454 let held = if gone { None } else { engine.frame_deadline(now) };
457 if let Some(at) = held {
458 wait = wait.min(at.saturating_sub(now));
459 }
460 if (engine.dirty && held.is_none() && !gone) || engine.has_queued_work() {
461 wait = Duration::ZERO;
462 }
463 if gone {
464 signals.wait(wait, false)?;
465 continue;
466 }
467 let mut input = Input { clipboard: &mut clipboard, late: &mut late };
468 match read_input(&mut engine, &mut input, signals, start, wait) {
469 Ok(true) => hang_up(signals, guard, &mut gone),
470 Ok(false) => {}
471 Err(error) => hang_up_or(error, signals, guard, &mut gone)?,
472 }
473 }
474}
475
476fn draw<A: App>(
480 terminal: &mut Screen<Stdout>,
481 engine: &mut Engine<A>,
482 clipboard: &mut TerminalClipboard,
483 start: Instant,
484) -> io::Result<()> {
485 let now = start.elapsed();
486 clipboard.update(engine, now)?;
487 engine.tick(now);
488 if engine.frame_due(now) {
489 terminal.present(|buffer| {
490 engine.render(buffer, start.elapsed());
491 engine.painted()
492 })?;
493 for text in engine.clipboard.drain(..) {
494 execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))?;
495 }
496 }
497 Ok(())
498}
499
500struct Input<'a> {
502 clipboard: &'a mut TerminalClipboard,
503 late: &'a mut LateAnswer,
504}
505
506fn read_input<A: App>(
509 engine: &mut Engine<A>,
510 input: &mut Input<'_>,
511 signals: &Signals,
512 start: Instant,
513 wait: Duration,
514) -> io::Result<bool> {
515 let Some(mut ready) = event_waiting(signals)? else {
518 return Ok(true);
519 };
520 if !ready && !wait.is_zero() {
521 let woken = signals.wait(wait, true)?;
522 if woken.hung_up {
523 return Ok(true);
524 }
525 if woken.keyboard {
526 let Some(waiting) = event_waiting(signals)? else {
527 return Ok(true);
528 };
529 ready = waiting;
530 }
531 }
532 while ready {
533 let event = ct::read()?;
534 if let ct::Event::Resize(..) = event {
535 engine.dirty = true;
536 took_cell(engine, cell_pixels());
538 }
539 let more = event_waiting(signals)?;
540 ready = more == Some(true);
541 for event in input.late.filter(event, ready, Instant::now()) {
542 for event in input.clipboard.filter(event, ready, engine, start.elapsed()) {
543 if let Some(event) = translate(event) {
544 engine.handle(event, start.elapsed());
545 }
546 }
547 }
548 if input.late.take_kitty() {
549 heard_kitty_late(engine);
550 }
551 if more.is_none() {
552 return Ok(true);
553 }
554 }
555 Ok(false)
556}
557
558fn cell_pixels() -> Option<(u16, u16)> {
561 let size = crossterm::terminal::window_size().ok()?;
562 cell_of(size.columns, size.rows, size.width, size.height)
563}
564
565fn cell_of(columns: u16, rows: u16, width: u16, height: u16) -> Option<(u16, u16)> {
568 let cell = (width.checked_div(columns)?, height.checked_div(rows)?);
569 (cell.0 > 0 && cell.1 > 0).then_some(cell)
570}
571
572fn took_cell<A: App>(engine: &mut Engine<A>, cell: Option<(u16, u16)>) {
575 if engine.env.cell_pixels() != cell {
576 engine.env.set_cell_pixels(cell);
577 engine.dirty = true;
578 }
579}
580
581fn heard_kitty_late<A: App>(engine: &mut Engine<A>) {
585 engine.env.set_terminal_graphics(crate::graphics::Graphics::Kitty);
586 engine.dirty = true;
587}
588
589fn event_waiting(signals: &Signals) -> io::Result<Option<bool>> {
593 if signals.hung_up_now() {
594 return Ok(None);
595 }
596 ct::poll(Duration::ZERO).map(Some)
597}
598
599fn hang_up_or(error: io::Error, signals: &Signals, guard: &TerminalGuard, gone: &mut bool) -> io::Result<()> {
602 if !signals.terminal_gone() {
603 return Err(error);
604 }
605 hang_up(signals, guard, gone);
606 Ok(())
607}
608
609fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
612 *gone = true;
613 guard.abandon();
614 signals.hung_up();
615}
616
617fn refuse_handoffs<A: App>(engine: &mut Engine<A>) {
620 const GONE: &str = "the terminal is gone";
621 while let Some(work) = engine.take_handoff() {
622 let message = match work {
623 HandOver::Wait(handoff) => handoff.finish(HandoffOutcome::Failed(GONE.to_owned())),
624 HandOver::Detach(handoff) => handoff.finish(DetachedOutcome::Failed(GONE.to_owned()), engine.deliveries()),
625 };
626 engine.update(message);
627 }
628}
629
630fn run_handoffs<A: App>(
635 terminal: &mut Screen<Stdout>,
636 engine: &mut Engine<A>,
637 guard: &TerminalGuard,
638 signals: &Signals,
639) {
640 while let Some(work) = engine.take_handoff() {
641 let _ = terminal.reset_pointer_shape();
644 #[cfg(feature = "image")]
646 let _ = terminal.release_pictures();
647 let prompt = engine.env.i18n().translate("quvyta.handoff.pause", &[]);
648 let deliveries = engine.deliveries();
649 let message = {
650 let mut release = |notice: Option<&str>| -> io::Result<()> {
651 guard.suspend()?;
652 let mut out = io::stdout();
653 execute!(out, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
654 if let Some(text) = notice {
655 writeln!(out, "{text}")?;
656 }
657 out.flush()
658 };
659 let mut take = || -> io::Result<()> {
660 let resumed = guard.resume();
663 let area = terminal.size()?;
669 terminal.redraw_all(area)?;
670 resumed
671 };
672 let mut wait_for_key = || wait_for_key_press(&prompt, signals);
673 let mut screen = HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key };
674 signals.handoff(true);
676 let message = match work {
677 HandOver::Wait(handoff) => handoff::run(handoff, &mut screen),
678 HandOver::Detach(handoff) => detached::run(handoff, &mut screen, &deliveries),
679 };
680 signals.handoff(false);
681 message
682 };
683 engine.dirty = true;
684 engine.update(message);
685 }
686}
687
688fn wait_for_key_press(prompt: &str, signals: &Signals) -> io::Result<()> {
690 let mut out = io::stdout();
691 write!(out, "\n{prompt}")?;
692 out.flush()?;
693 enable_raw_mode()?;
695 let pressed = wait_for_key(signals);
696 disable_raw_mode()?;
697 writeln!(out)?;
698 pressed
699}
700
701fn wait_for_key(signals: &Signals) -> io::Result<()> {
704 loop {
705 if signals.pending() {
706 return Ok(());
707 }
708 match event_waiting(signals)? {
709 None => return Ok(()),
711 Some(true) => {
712 if let ct::Event::Key(key) = ct::read()?
713 && key.kind == ct::KeyEventKind::Press
714 {
715 return Ok(());
716 }
717 }
718 Some(false) => {
719 if signals.wait(IDLE_WAIT, true)?.hung_up {
720 return Ok(());
721 }
722 }
723 }
724 }
725}
726
727struct TerminalGuard {
731 keyboard_enhanced: bool,
732 abandoned: Cell<bool>,
735}
736
737impl TerminalGuard {
738 fn enter() -> io::Result<Self> {
739 enable_raw_mode()?;
740 let guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
743 execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
744 Ok(guard)
745 }
746
747 fn enhance_keyboard(&mut self) -> io::Result<()> {
752 self.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
753 self.push_keyboard_flags()
754 }
755
756 fn suspend(&self) -> io::Result<()> {
758 release(self.keyboard_enhanced)
759 }
760
761 fn resume(&self) -> io::Result<()> {
764 take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
765 }
766
767 fn abandon(&self) {
769 self.abandoned.set(true);
770 }
771
772 fn push_keyboard_flags(&self) -> io::Result<()> {
773 push_keyboard_flags(&mut io::stdout(), self.keyboard_enhanced)
774 }
775}
776
777fn take_back(out: &mut impl Write, keyboard_enhanced: bool, raw_on: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
782 let raw = raw_on();
783 let screen = execute!(out, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide);
784 let flags = push_keyboard_flags(out, keyboard_enhanced);
785 raw.and(screen).and(flags)
786}
787
788fn push_keyboard_flags(out: &mut impl Write, keyboard_enhanced: bool) -> io::Result<()> {
789 if keyboard_enhanced {
790 execute!(
791 out,
792 PushKeyboardEnhancementFlags(
793 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
794 )
795 )?;
796 }
797 Ok(())
798}
799
800impl Drop for TerminalGuard {
801 fn drop(&mut self) {
802 if !self.abandoned.get() {
803 restore(self.keyboard_enhanced);
804 }
805 }
806}
807
808fn release(keyboard_enhanced: bool) -> io::Result<()> {
810 give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
811}
812
813pub(super) fn give_back(
818 out: &mut impl Write,
819 keyboard_enhanced: bool,
820 raw_off: impl FnOnce() -> io::Result<()>,
821) -> io::Result<()> {
822 let flags = if keyboard_enhanced { execute!(out, PopKeyboardEnhancementFlags) } else { Ok(()) };
823 let screen = execute!(out, DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen, cursor::Show);
824 let raw = raw_off();
825 let flushed = out.flush();
826 flags.and(screen).and(raw).and(flushed)
827}
828
829fn restore(keyboard_enhanced: bool) {
831 let _ = release(keyboard_enhanced);
832}
833
834fn install_panic_hook() {
835 on_panic_in_this_thread(|| restore(true));
836}
837
838fn on_panic_in_this_thread(on_panic: impl Fn() + Send + Sync + 'static) {
843 let owner = std::thread::current().id();
844 let previous = std::panic::take_hook();
845 std::panic::set_hook(Box::new(move |info| {
846 if std::thread::current().id() == owner {
847 on_panic();
848 }
849 previous(info);
850 }));
851}
852
853fn translate(event: ct::Event) -> Option<Event> {
855 match event {
856 ct::Event::Key(key) => translate_key(key).map(Event::Key),
857 ct::Event::Mouse(mouse) => translate_mouse(mouse).map(Event::Mouse),
858 ct::Event::Paste(text) => Some(Event::Paste(text)),
859 ct::Event::FocusGained | ct::Event::FocusLost | ct::Event::Resize(..) => None,
860 }
861}
862
863fn modifiers(mods: ct::KeyModifiers) -> Modifiers {
864 Modifiers {
865 ctrl: mods.contains(ct::KeyModifiers::CONTROL),
866 alt: mods.contains(ct::KeyModifiers::ALT),
867 shift: mods.contains(ct::KeyModifiers::SHIFT),
868 }
869}
870
871fn translate_key(key: ct::KeyEvent) -> Option<KeyEvent> {
872 let mut mods = modifiers(key.modifiers);
873 let code = match key.code {
874 ct::KeyCode::Char(' ') => Key::Space,
875 ct::KeyCode::Char(c) if c.is_uppercase() => {
876 mods.shift = true;
877 Key::Char(c.to_lowercase().next().unwrap_or(c))
878 }
879 ct::KeyCode::Char(c) => {
880 if !c.is_alphabetic() {
881 mods.shift = false;
882 }
883 Key::Char(c)
884 }
885 ct::KeyCode::Enter => Key::Enter,
886 ct::KeyCode::Esc => Key::Esc,
887 ct::KeyCode::Tab => Key::Tab,
888 ct::KeyCode::BackTab => {
889 mods.shift = true;
890 Key::Tab
891 }
892 ct::KeyCode::Backspace => Key::Backspace,
893 ct::KeyCode::Delete => Key::Delete,
894 ct::KeyCode::Insert => Key::Insert,
895 ct::KeyCode::Home => Key::Home,
896 ct::KeyCode::End => Key::End,
897 ct::KeyCode::PageUp => Key::PageUp,
898 ct::KeyCode::PageDown => Key::PageDown,
899 ct::KeyCode::Up => Key::Up,
900 ct::KeyCode::Down => Key::Down,
901 ct::KeyCode::Left => Key::Left,
902 ct::KeyCode::Right => Key::Right,
903 ct::KeyCode::F(n) => Key::F(n),
904 ct::KeyCode::Menu => Key::Menu,
905 _ => return None,
906 };
907 let kind = match key.kind {
908 ct::KeyEventKind::Press => KeyKind::Press,
909 ct::KeyEventKind::Repeat => KeyKind::Repeat,
910 ct::KeyEventKind::Release => KeyKind::Release,
911 };
912 let text = match key.code {
913 ct::KeyCode::Char(c) if !mods.ctrl && !mods.alt => Some(c),
914 _ => None,
915 };
916 Some(KeyEvent { chord: KeyChord { key: code, mods }, kind, text })
917}
918
919fn translate_mouse(mouse: ct::MouseEvent) -> Option<MouseEvent> {
920 let button = |b: ct::MouseButton| match b {
921 ct::MouseButton::Left => MouseButton::Left,
922 ct::MouseButton::Right => MouseButton::Right,
923 ct::MouseButton::Middle => MouseButton::Middle,
924 };
925 let kind = match mouse.kind {
926 ct::MouseEventKind::Down(b) => MouseKind::Down(button(b)),
927 ct::MouseEventKind::Up(b) => MouseKind::Up(button(b)),
928 ct::MouseEventKind::Drag(b) => MouseKind::Drag(button(b)),
929 ct::MouseEventKind::Moved => MouseKind::Moved,
930 ct::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
931 ct::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
932 ct::MouseEventKind::ScrollLeft | ct::MouseEventKind::ScrollRight => return None,
933 };
934 Some(MouseEvent { kind, x: i32::from(mouse.column), y: i32::from(mouse.row), mods: modifiers(mouse.modifiers) })
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940
941 struct Told(Vec<crate::graphics::Graphics>);
943
944 impl App for Told {
945 type Msg = crate::graphics::Graphics;
946 fn update(&mut self, graphics: Self::Msg) -> crate::runtime::Command<Self::Msg> {
947 self.0.push(graphics);
948 crate::runtime::Command::none()
949 }
950 fn view(&self, _ui: &mut crate::widget::View<'_, Self::Msg>) {}
951 fn graphics(&self, graphics: crate::graphics::Graphics) -> Option<Self::Msg> {
952 Some(graphics)
953 }
954 }
955
956 #[test]
957 fn a_late_kitty_answer_turns_pictures_to_kitty_and_the_application_hears_it() {
958 use crate::graphics::Graphics;
959 let mut engine = Engine::new(Told(Vec::new()), Env::builtin(), TaskMode::Inline);
960 let area = ratatui_core::layout::Rect::new(0, 0, 10, 4);
961 let mut buffer = ratatui_core::buffer::Buffer::empty(area);
962 engine.render(&mut buffer, Duration::ZERO);
963 assert_eq!(engine.app.0, [Graphics::HalfBlock], "no answer in time");
964 engine.dirty = false;
965 heard_kitty_late(&mut engine);
966 assert!(engine.dirty, "a frame is due");
967 engine.render(&mut buffer, Duration::from_secs(1));
968 assert_eq!(engine.app.0, [Graphics::HalfBlock, Graphics::Kitty]);
969 assert_eq!(engine.env.graphics(), Graphics::Kitty);
970 }
971
972 #[test]
973 fn a_cell_is_the_window_divided_by_its_columns_and_rows() {
974 assert_eq!(cell_of(80, 24, 800, 480), Some((10, 20)));
975 assert_eq!(cell_of(100, 30, 905, 571), Some((9, 19)), "a margin is left out");
976 assert_eq!(cell_of(80, 24, 0, 0), None, "a terminal that reports no pixels");
977 assert_eq!(cell_of(0, 0, 800, 480), None);
978 assert_eq!(cell_of(80, 24, 40, 480), None, "fewer pixels than columns");
979 }
980
981 struct Seen(std::cell::RefCell<Vec<Option<(u16, u16)>>>);
983
984 impl App for Seen {
985 type Msg = ();
986 fn update(&mut self, (): ()) -> crate::runtime::Command<()> {
987 crate::runtime::Command::none()
988 }
989 fn view(&self, ui: &mut crate::widget::View<'_, ()>) {
990 self.0.borrow_mut().push(ui.env().cell_pixels());
991 }
992 }
993
994 #[test]
995 fn a_new_cell_size_is_a_new_frame_and_the_same_one_is_not() {
996 let mut engine = Engine::new(Seen(std::cell::RefCell::default()), Env::builtin(), TaskMode::Inline);
997 took_cell(&mut engine, Some((10, 20)));
998 let area = ratatui_core::layout::Rect::new(0, 0, 10, 4);
999 let mut buffer = ratatui_core::buffer::Buffer::empty(area);
1000 engine.render(&mut buffer, Duration::ZERO);
1001 engine.dirty = false;
1002 took_cell(&mut engine, Some((10, 20)));
1003 assert!(!engine.dirty, "nothing changed");
1004 took_cell(&mut engine, Some((14, 28)));
1005 assert!(engine.dirty, "a font grew under the same columns and rows");
1006 engine.render(&mut buffer, Duration::from_secs(1));
1007 assert_eq!(*engine.app.0.borrow(), [Some((10, 20)), Some((14, 28))]);
1008 }
1009
1010 #[test]
1011 fn panics_on_other_threads_leave_the_terminal_alone() {
1012 use std::sync::Arc;
1013 use std::sync::atomic::{AtomicUsize, Ordering};
1014 let restores = Arc::new(AtomicUsize::new(0));
1015 let counter = Arc::clone(&restores);
1016 on_panic_in_this_thread(move || {
1017 counter.fetch_add(1, Ordering::SeqCst);
1018 });
1019 let _ = std::thread::spawn(|| panic!("a background task failed")).join();
1021 assert_eq!(restores.load(Ordering::SeqCst), 0, "the terminal stays in application mode");
1022 let _ = std::panic::catch_unwind(|| panic!("the runtime failed"));
1023 assert_eq!(restores.load(Ordering::SeqCst), 1, "a panic of the runtime thread restores it");
1024 }
1025
1026 struct Broken;
1028
1029 impl Write for Broken {
1030 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
1031 Err(io::Error::other("the terminal is gone"))
1032 }
1033
1034 fn flush(&mut self) -> io::Result<()> {
1035 Err(io::Error::other("the terminal is gone"))
1036 }
1037 }
1038
1039 #[test]
1040 fn raw_mode_is_left_even_when_the_screen_cannot_be_written() {
1041 let mut raw_left = false;
1042 let result = give_back(&mut Broken, true, || {
1043 raw_left = true;
1044 Ok(())
1045 });
1046 assert!(raw_left, "raw mode is a terminal setting, not output, and is always left");
1047 assert_eq!(result.expect_err("the failure is reported").to_string(), "the terminal is gone");
1048 }
1049
1050 #[test]
1051 fn leaving_application_mode_writes_every_step_after_one_fails() {
1052 let mut out = Vec::new();
1053 let result = give_back(&mut out, true, || Err(io::Error::other("no raw mode")));
1054 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
1055 let text = String::from_utf8(out).expect("escape codes");
1056 assert!(text.contains("\x1b[?1049l"), "the alternate screen was left: {text:?}");
1057 assert!(text.contains("\x1b[?25h"), "the cursor is shown again: {text:?}");
1058 }
1059
1060 #[test]
1061 fn taking_the_terminal_back_goes_on_when_raw_mode_fails() {
1062 let mut out = Vec::new();
1064 let result = take_back(&mut out, false, || Err(io::Error::other("no raw mode")));
1065 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
1066 let text = String::from_utf8(out).expect("escape codes");
1067 assert!(text.contains("\x1b[?1049h"), "the alternate screen is entered again: {text:?}");
1068 }
1069
1070 #[test]
1071 fn translates_uppercase_and_backtab() {
1072 let key = |code, mods| ct::KeyEvent::new(code, mods);
1073 let a = translate_key(key(ct::KeyCode::Char('A'), ct::KeyModifiers::SHIFT)).expect("key");
1074 assert_eq!(a.chord, "shift+a".parse().expect("chord"));
1075 assert_eq!(a.text, Some('A'));
1076 let question = translate_key(key(ct::KeyCode::Char('?'), ct::KeyModifiers::SHIFT)).expect("key");
1077 assert_eq!(question.chord, "?".parse().expect("chord"));
1078 let back = translate_key(key(ct::KeyCode::BackTab, ct::KeyModifiers::SHIFT)).expect("key");
1079 assert_eq!(back.chord, "shift+tab".parse().expect("chord"));
1080 let ctrl = translate_key(key(ct::KeyCode::Char('q'), ct::KeyModifiers::CONTROL)).expect("key");
1081 assert_eq!(ctrl.chord, "ctrl+q".parse().expect("chord"));
1082 assert_eq!(ctrl.text, None);
1083 let menu = translate_key(key(ct::KeyCode::Menu, ct::KeyModifiers::NONE)).expect("key");
1084 assert_eq!(menu.chord, "menu".parse().expect("chord"));
1085 }
1086}