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 mut screen = Screen::new(Terminal::new(CrosstermBackend::new(io::stdout()))?).pointer_shapes(shapes);
249 let engine = Engine::new(self.app, env, TaskMode::Threads);
250 let result = event_loop(&mut screen, engine, &guard, &signals, late);
251 if !guard.abandoned.get() {
252 let _ = screen.reset_pointer_shape();
255 #[cfg(feature = "image")]
257 let _ = screen.release_pictures();
258 }
259 let terminal = screen.into_terminal();
260 if guard.abandoned.get() {
261 std::mem::forget(terminal);
263 } else {
264 drop(terminal);
265 }
266 drop(guard);
267 drop(signals);
268 result
269 }
270}
271
272#[cfg(unix)]
276fn ask_graphics(env: &mut Env, signals: &Signals) -> LateAnswer {
277 use super::graphics_probe::{PROBE_WAIT, late_from, probe};
278 use rustix::termios::isatty;
279 if !env.graphics_worth_asking() || !isatty(signals.tty()) || !isatty(io::stdout()) {
280 return LateAnswer::default();
281 }
282 match probe(signals.tty(), &mut io::stdout(), PROBE_WAIT) {
283 Ok(probe) => {
284 env.set_terminal_graphics(probe.graphics);
285 if probe.answered { LateAnswer::default() } else { late_from(Instant::now()) }
286 }
287 Err(_) => late_from(Instant::now()),
289 }
290}
291
292#[cfg(not(unix))]
295fn ask_graphics(_env: &mut Env, _signals: &Signals) -> LateAnswer {
296 LateAnswer::default()
297}
298
299fn event_loop<A: App>(
300 terminal: &mut Screen<Stdout>,
301 mut engine: Engine<A>,
302 guard: &TerminalGuard,
303 signals: &Signals,
304 mut late: LateAnswer,
305) -> io::Result<()> {
306 let start = Instant::now();
307 let mut clipboard = TerminalClipboard::default();
308 let mut gone = false;
311 loop {
312 let now = start.elapsed();
313 let heard = signals.take();
314 if heard.resized {
315 engine.dirty = true;
318 }
319 for cause in heard.causes {
320 if cause == Termination::Hangup && !gone && signals.terminal_gone() {
321 gone = true;
322 guard.abandon();
323 }
324 engine.terminate(cause, now);
325 }
326 engine.poll_tasks();
327 engine.run_queued_work();
328 if gone {
329 refuse_handoffs(&mut engine);
330 } else {
331 run_handoffs(terminal, &mut engine, guard, signals);
332 if let Err(error) = draw(terminal, &mut engine, &mut clipboard, start) {
334 hang_up_or(error, signals, guard, &mut gone)?;
335 }
336 }
337 engine.end_when_due(start.elapsed());
338 if engine.quit {
339 return Ok(());
340 }
341 let now = start.elapsed();
342 let mut wait = match (gone, engine.deadline()) {
343 (true, _) | (false, None) => IDLE_WAIT,
345 (false, Some(deadline)) => deadline.saturating_sub(now),
346 };
347 if let Some(deadline) = engine.ending_deadline() {
348 wait = wait.min(deadline.saturating_sub(now));
349 }
350 if engine.pending_tasks > 0 || (!gone && engine.clipboard_reader.is_reading()) {
351 wait = wait.min(TASK_WAIT);
352 }
353 if let Some(deadline) = clipboard.deadline().filter(|_| !gone) {
354 wait = wait.min(deadline.saturating_sub(now));
355 }
356 let held = if gone { None } else { engine.frame_deadline(now) };
359 if let Some(at) = held {
360 wait = wait.min(at.saturating_sub(now));
361 }
362 if (engine.dirty && held.is_none() && !gone) || engine.has_queued_work() {
363 wait = Duration::ZERO;
364 }
365 if gone {
366 signals.wait(wait, false)?;
367 continue;
368 }
369 let mut input = Input { clipboard: &mut clipboard, late: &mut late };
370 match read_input(&mut engine, &mut input, signals, start, wait) {
371 Ok(true) => hang_up(signals, guard, &mut gone),
372 Ok(false) => {}
373 Err(error) => hang_up_or(error, signals, guard, &mut gone)?,
374 }
375 }
376}
377
378fn draw<A: App>(
382 terminal: &mut Screen<Stdout>,
383 engine: &mut Engine<A>,
384 clipboard: &mut TerminalClipboard,
385 start: Instant,
386) -> io::Result<()> {
387 let now = start.elapsed();
388 clipboard.update(engine, now)?;
389 engine.tick(now);
390 if engine.frame_due(now) {
391 terminal.present(|buffer| {
392 engine.render(buffer, start.elapsed());
393 engine.painted()
394 })?;
395 for text in engine.clipboard.drain(..) {
396 execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))?;
397 }
398 }
399 Ok(())
400}
401
402struct Input<'a> {
404 clipboard: &'a mut TerminalClipboard,
405 late: &'a mut LateAnswer,
406}
407
408fn read_input<A: App>(
411 engine: &mut Engine<A>,
412 input: &mut Input<'_>,
413 signals: &Signals,
414 start: Instant,
415 wait: Duration,
416) -> io::Result<bool> {
417 let Some(mut ready) = event_waiting(signals)? else {
420 return Ok(true);
421 };
422 if !ready && !wait.is_zero() {
423 let woken = signals.wait(wait, true)?;
424 if woken.hung_up {
425 return Ok(true);
426 }
427 if woken.keyboard {
428 let Some(waiting) = event_waiting(signals)? else {
429 return Ok(true);
430 };
431 ready = waiting;
432 }
433 }
434 while ready {
435 let event = ct::read()?;
436 if let ct::Event::Resize(..) = event {
437 engine.dirty = true;
438 }
439 let more = event_waiting(signals)?;
440 ready = more == Some(true);
441 for event in input.late.filter(event, ready, Instant::now()) {
442 for event in input.clipboard.filter(event, ready, engine, start.elapsed()) {
443 if let Some(event) = translate(event) {
444 engine.handle(event, start.elapsed());
445 }
446 }
447 }
448 if more.is_none() {
449 return Ok(true);
450 }
451 }
452 Ok(false)
453}
454
455fn event_waiting(signals: &Signals) -> io::Result<Option<bool>> {
459 if signals.hung_up_now() {
460 return Ok(None);
461 }
462 ct::poll(Duration::ZERO).map(Some)
463}
464
465fn hang_up_or(error: io::Error, signals: &Signals, guard: &TerminalGuard, gone: &mut bool) -> io::Result<()> {
468 if !signals.terminal_gone() {
469 return Err(error);
470 }
471 hang_up(signals, guard, gone);
472 Ok(())
473}
474
475fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
478 *gone = true;
479 guard.abandon();
480 signals.hung_up();
481}
482
483fn refuse_handoffs<A: App>(engine: &mut Engine<A>) {
486 const GONE: &str = "the terminal is gone";
487 while let Some(work) = engine.take_handoff() {
488 let message = match work {
489 HandOver::Wait(handoff) => handoff.finish(HandoffOutcome::Failed(GONE.to_owned())),
490 HandOver::Detach(handoff) => handoff.finish(DetachedOutcome::Failed(GONE.to_owned()), engine.deliveries()),
491 };
492 engine.update(message);
493 }
494}
495
496fn run_handoffs<A: App>(
501 terminal: &mut Screen<Stdout>,
502 engine: &mut Engine<A>,
503 guard: &TerminalGuard,
504 signals: &Signals,
505) {
506 while let Some(work) = engine.take_handoff() {
507 let _ = terminal.reset_pointer_shape();
510 #[cfg(feature = "image")]
512 let _ = terminal.release_pictures();
513 let prompt = engine.env.i18n().translate("quvyta.handoff.pause", &[]);
514 let deliveries = engine.deliveries();
515 let message = {
516 let mut release = |notice: Option<&str>| -> io::Result<()> {
517 guard.suspend()?;
518 let mut out = io::stdout();
519 execute!(out, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
520 if let Some(text) = notice {
521 writeln!(out, "{text}")?;
522 }
523 out.flush()
524 };
525 let mut take = || -> io::Result<()> {
526 let resumed = guard.resume();
529 let area = terminal.size()?;
535 terminal.redraw_all(area)?;
536 resumed
537 };
538 let mut wait_for_key = || wait_for_key_press(&prompt, signals);
539 let mut screen = HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key };
540 signals.handoff(true);
542 let message = match work {
543 HandOver::Wait(handoff) => handoff::run(handoff, &mut screen),
544 HandOver::Detach(handoff) => detached::run(handoff, &mut screen, &deliveries),
545 };
546 signals.handoff(false);
547 message
548 };
549 engine.dirty = true;
550 engine.update(message);
551 }
552}
553
554fn wait_for_key_press(prompt: &str, signals: &Signals) -> io::Result<()> {
556 let mut out = io::stdout();
557 write!(out, "\n{prompt}")?;
558 out.flush()?;
559 enable_raw_mode()?;
561 let pressed = wait_for_key(signals);
562 disable_raw_mode()?;
563 writeln!(out)?;
564 pressed
565}
566
567fn wait_for_key(signals: &Signals) -> io::Result<()> {
570 loop {
571 if signals.pending() {
572 return Ok(());
573 }
574 match event_waiting(signals)? {
575 None => return Ok(()),
577 Some(true) => {
578 if let ct::Event::Key(key) = ct::read()?
579 && key.kind == ct::KeyEventKind::Press
580 {
581 return Ok(());
582 }
583 }
584 Some(false) => {
585 if signals.wait(IDLE_WAIT, true)?.hung_up {
586 return Ok(());
587 }
588 }
589 }
590 }
591}
592
593struct TerminalGuard {
597 keyboard_enhanced: bool,
598 abandoned: Cell<bool>,
601}
602
603impl TerminalGuard {
604 fn enter() -> io::Result<Self> {
605 enable_raw_mode()?;
606 let guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
609 execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
610 Ok(guard)
611 }
612
613 fn enhance_keyboard(&mut self) -> io::Result<()> {
618 self.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
619 self.push_keyboard_flags()
620 }
621
622 fn suspend(&self) -> io::Result<()> {
624 release(self.keyboard_enhanced)
625 }
626
627 fn resume(&self) -> io::Result<()> {
630 take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
631 }
632
633 fn abandon(&self) {
635 self.abandoned.set(true);
636 }
637
638 fn push_keyboard_flags(&self) -> io::Result<()> {
639 push_keyboard_flags(&mut io::stdout(), self.keyboard_enhanced)
640 }
641}
642
643fn take_back(out: &mut impl Write, keyboard_enhanced: bool, raw_on: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
648 let raw = raw_on();
649 let screen = execute!(out, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide);
650 let flags = push_keyboard_flags(out, keyboard_enhanced);
651 raw.and(screen).and(flags)
652}
653
654fn push_keyboard_flags(out: &mut impl Write, keyboard_enhanced: bool) -> io::Result<()> {
655 if keyboard_enhanced {
656 execute!(
657 out,
658 PushKeyboardEnhancementFlags(
659 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
660 )
661 )?;
662 }
663 Ok(())
664}
665
666impl Drop for TerminalGuard {
667 fn drop(&mut self) {
668 if !self.abandoned.get() {
669 restore(self.keyboard_enhanced);
670 }
671 }
672}
673
674fn release(keyboard_enhanced: bool) -> io::Result<()> {
676 give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
677}
678
679pub(super) fn give_back(
684 out: &mut impl Write,
685 keyboard_enhanced: bool,
686 raw_off: impl FnOnce() -> io::Result<()>,
687) -> io::Result<()> {
688 let flags = if keyboard_enhanced { execute!(out, PopKeyboardEnhancementFlags) } else { Ok(()) };
689 let screen = execute!(out, DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen, cursor::Show);
690 let raw = raw_off();
691 let flushed = out.flush();
692 flags.and(screen).and(raw).and(flushed)
693}
694
695fn restore(keyboard_enhanced: bool) {
697 let _ = release(keyboard_enhanced);
698}
699
700fn install_panic_hook() {
701 on_panic_in_this_thread(|| restore(true));
702}
703
704fn on_panic_in_this_thread(on_panic: impl Fn() + Send + Sync + 'static) {
709 let owner = std::thread::current().id();
710 let previous = std::panic::take_hook();
711 std::panic::set_hook(Box::new(move |info| {
712 if std::thread::current().id() == owner {
713 on_panic();
714 }
715 previous(info);
716 }));
717}
718
719fn translate(event: ct::Event) -> Option<Event> {
721 match event {
722 ct::Event::Key(key) => translate_key(key).map(Event::Key),
723 ct::Event::Mouse(mouse) => translate_mouse(mouse).map(Event::Mouse),
724 ct::Event::Paste(text) => Some(Event::Paste(text)),
725 ct::Event::FocusGained | ct::Event::FocusLost | ct::Event::Resize(..) => None,
726 }
727}
728
729fn modifiers(mods: ct::KeyModifiers) -> Modifiers {
730 Modifiers {
731 ctrl: mods.contains(ct::KeyModifiers::CONTROL),
732 alt: mods.contains(ct::KeyModifiers::ALT),
733 shift: mods.contains(ct::KeyModifiers::SHIFT),
734 }
735}
736
737fn translate_key(key: ct::KeyEvent) -> Option<KeyEvent> {
738 let mut mods = modifiers(key.modifiers);
739 let code = match key.code {
740 ct::KeyCode::Char(' ') => Key::Space,
741 ct::KeyCode::Char(c) if c.is_uppercase() => {
742 mods.shift = true;
743 Key::Char(c.to_lowercase().next().unwrap_or(c))
744 }
745 ct::KeyCode::Char(c) => {
746 if !c.is_alphabetic() {
747 mods.shift = false;
748 }
749 Key::Char(c)
750 }
751 ct::KeyCode::Enter => Key::Enter,
752 ct::KeyCode::Esc => Key::Esc,
753 ct::KeyCode::Tab => Key::Tab,
754 ct::KeyCode::BackTab => {
755 mods.shift = true;
756 Key::Tab
757 }
758 ct::KeyCode::Backspace => Key::Backspace,
759 ct::KeyCode::Delete => Key::Delete,
760 ct::KeyCode::Insert => Key::Insert,
761 ct::KeyCode::Home => Key::Home,
762 ct::KeyCode::End => Key::End,
763 ct::KeyCode::PageUp => Key::PageUp,
764 ct::KeyCode::PageDown => Key::PageDown,
765 ct::KeyCode::Up => Key::Up,
766 ct::KeyCode::Down => Key::Down,
767 ct::KeyCode::Left => Key::Left,
768 ct::KeyCode::Right => Key::Right,
769 ct::KeyCode::F(n) => Key::F(n),
770 ct::KeyCode::Menu => Key::Menu,
771 _ => return None,
772 };
773 let kind = match key.kind {
774 ct::KeyEventKind::Press => KeyKind::Press,
775 ct::KeyEventKind::Repeat => KeyKind::Repeat,
776 ct::KeyEventKind::Release => KeyKind::Release,
777 };
778 let text = match key.code {
779 ct::KeyCode::Char(c) if !mods.ctrl && !mods.alt => Some(c),
780 _ => None,
781 };
782 Some(KeyEvent { chord: KeyChord { key: code, mods }, kind, text })
783}
784
785fn translate_mouse(mouse: ct::MouseEvent) -> Option<MouseEvent> {
786 let button = |b: ct::MouseButton| match b {
787 ct::MouseButton::Left => MouseButton::Left,
788 ct::MouseButton::Right => MouseButton::Right,
789 ct::MouseButton::Middle => MouseButton::Middle,
790 };
791 let kind = match mouse.kind {
792 ct::MouseEventKind::Down(b) => MouseKind::Down(button(b)),
793 ct::MouseEventKind::Up(b) => MouseKind::Up(button(b)),
794 ct::MouseEventKind::Drag(b) => MouseKind::Drag(button(b)),
795 ct::MouseEventKind::Moved => MouseKind::Moved,
796 ct::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
797 ct::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
798 ct::MouseEventKind::ScrollLeft | ct::MouseEventKind::ScrollRight => return None,
799 };
800 Some(MouseEvent { kind, x: i32::from(mouse.column), y: i32::from(mouse.row), mods: modifiers(mouse.modifiers) })
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806
807 #[test]
808 fn panics_on_other_threads_leave_the_terminal_alone() {
809 use std::sync::Arc;
810 use std::sync::atomic::{AtomicUsize, Ordering};
811 let restores = Arc::new(AtomicUsize::new(0));
812 let counter = Arc::clone(&restores);
813 on_panic_in_this_thread(move || {
814 counter.fetch_add(1, Ordering::SeqCst);
815 });
816 let _ = std::thread::spawn(|| panic!("a background task failed")).join();
818 assert_eq!(restores.load(Ordering::SeqCst), 0, "the terminal stays in application mode");
819 let _ = std::panic::catch_unwind(|| panic!("the runtime failed"));
820 assert_eq!(restores.load(Ordering::SeqCst), 1, "a panic of the runtime thread restores it");
821 }
822
823 struct Broken;
825
826 impl Write for Broken {
827 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
828 Err(io::Error::other("the terminal is gone"))
829 }
830
831 fn flush(&mut self) -> io::Result<()> {
832 Err(io::Error::other("the terminal is gone"))
833 }
834 }
835
836 #[test]
837 fn raw_mode_is_left_even_when_the_screen_cannot_be_written() {
838 let mut raw_left = false;
839 let result = give_back(&mut Broken, true, || {
840 raw_left = true;
841 Ok(())
842 });
843 assert!(raw_left, "raw mode is a terminal setting, not output, and is always left");
844 assert_eq!(result.expect_err("the failure is reported").to_string(), "the terminal is gone");
845 }
846
847 #[test]
848 fn leaving_application_mode_writes_every_step_after_one_fails() {
849 let mut out = Vec::new();
850 let result = give_back(&mut out, true, || Err(io::Error::other("no raw mode")));
851 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
852 let text = String::from_utf8(out).expect("escape codes");
853 assert!(text.contains("\x1b[?1049l"), "the alternate screen was left: {text:?}");
854 assert!(text.contains("\x1b[?25h"), "the cursor is shown again: {text:?}");
855 }
856
857 #[test]
858 fn taking_the_terminal_back_goes_on_when_raw_mode_fails() {
859 let mut out = Vec::new();
861 let result = take_back(&mut out, false, || Err(io::Error::other("no raw mode")));
862 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
863 let text = String::from_utf8(out).expect("escape codes");
864 assert!(text.contains("\x1b[?1049h"), "the alternate screen is entered again: {text:?}");
865 }
866
867 #[test]
868 fn translates_uppercase_and_backtab() {
869 let key = |code, mods| ct::KeyEvent::new(code, mods);
870 let a = translate_key(key(ct::KeyCode::Char('A'), ct::KeyModifiers::SHIFT)).expect("key");
871 assert_eq!(a.chord, "shift+a".parse().expect("chord"));
872 assert_eq!(a.text, Some('A'));
873 let question = translate_key(key(ct::KeyCode::Char('?'), ct::KeyModifiers::SHIFT)).expect("key");
874 assert_eq!(question.chord, "?".parse().expect("chord"));
875 let back = translate_key(key(ct::KeyCode::BackTab, ct::KeyModifiers::SHIFT)).expect("key");
876 assert_eq!(back.chord, "shift+tab".parse().expect("chord"));
877 let ctrl = translate_key(key(ct::KeyCode::Char('q'), ct::KeyModifiers::CONTROL)).expect("key");
878 assert_eq!(ctrl.chord, "ctrl+q".parse().expect("chord"));
879 assert_eq!(ctrl.text, None);
880 let menu = translate_key(key(ct::KeyCode::Menu, ct::KeyModifiers::NONE)).expect("key");
881 assert_eq!(menu.chord, "menu".parse().expect("chord"));
882 }
883}