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 BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
15 disable_raw_mode, enable_raw_mode, 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::handoff::{self, HandoffOutcome, HandoffScreen};
25use super::signals::Signals;
26use super::terminal_clipboard::TerminalClipboard;
27use super::termination::Termination;
28use crate::env::{AssetDirs, Env};
29use crate::event::{Event, KeyEvent, KeyKind, MouseButton, MouseEvent, MouseKind};
30use crate::keymap::{Key, KeyChord, Modifiers};
31use crate::storage::{Preferences, Settings};
32
33const IDLE_WAIT: Duration = Duration::from_millis(500);
35const TASK_WAIT: Duration = Duration::from_millis(20);
37
38pub struct Runtime<A: App> {
40 app: A,
41 dirs: AssetDirs,
42 theme: Option<String>,
43 settings: Option<Settings>,
44 preferences: Option<Preferences>,
45}
46
47impl<A: App> Runtime<A> {
48 pub fn new(app: A) -> Self {
50 Self { app, dirs: AssetDirs::default(), theme: None, settings: None, preferences: None }
51 }
52
53 #[must_use]
55 pub fn theme_dir(mut self, dir: impl Into<PathBuf>) -> Self {
56 self.dirs.themes = Some(dir.into());
57 self
58 }
59
60 #[must_use]
65 pub fn theme_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
66 self.dirs.theme_sources.push((file.into(), text.into()));
67 self
68 }
69
70 #[must_use]
72 pub fn icon_dir(mut self, dir: impl Into<PathBuf>) -> Self {
73 self.dirs.icons = Some(dir.into());
74 self
75 }
76
77 #[must_use]
88 pub fn icon_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
89 self.dirs.icon_sources.push((file.into(), text.into()));
90 self
91 }
92
93 #[must_use]
95 pub fn locale_dir(mut self, dir: impl Into<PathBuf>) -> Self {
96 self.dirs.locales = Some(dir.into());
97 self
98 }
99
100 #[must_use]
118 pub fn locale_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
119 self.dirs.locale_sources.push((file.into(), text.into()));
120 self
121 }
122
123 #[must_use]
125 pub fn keymap_file(mut self, file: impl Into<PathBuf>) -> Self {
126 self.dirs.keymap = Some(file.into());
127 self
128 }
129
130 #[must_use]
148 pub fn keymap_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
149 self.dirs.keymap_source = Some((file.into(), text.into()));
150 self
151 }
152
153 #[must_use]
155 pub fn theme(mut self, id: impl Into<String>) -> Self {
156 self.theme = Some(id.into());
157 self
158 }
159
160 #[must_use]
164 pub fn settings(mut self, settings: &Settings) -> Self {
165 self.settings = Some(settings.clone());
166 self
167 }
168
169 #[must_use]
192 pub fn preferences(mut self, preferences: &Preferences) -> Self {
193 self.preferences = Some(preferences.clone());
194 self
195 }
196
197 pub fn run(self) -> io::Result<()> {
226 let mut env = Env::load(&self.dirs)?;
227 if let Some(theme) = &self.theme {
228 env.set_theme(theme);
229 }
230 if let Some(settings) = &self.settings {
231 env.apply_settings(settings);
232 }
233 if let Some(preferences) = &self.preferences {
234 env.apply_preferences(preferences);
235 }
236 let signals = Signals::catch()?;
239 let guard = TerminalGuard::enter()?;
240 install_panic_hook();
241 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
242 let result = event_loop(&mut terminal, Engine::new(self.app, env, TaskMode::Threads), &guard, &signals);
243 if guard.abandoned.get() {
244 std::mem::forget(terminal);
246 } else {
247 drop(terminal);
248 }
249 drop(guard);
250 drop(signals);
251 result
252 }
253}
254
255fn event_loop<A: App>(
256 terminal: &mut Terminal<CrosstermBackend<Stdout>>,
257 mut engine: Engine<A>,
258 guard: &TerminalGuard,
259 signals: &Signals,
260) -> io::Result<()> {
261 let start = Instant::now();
262 let mut clipboard = TerminalClipboard::default();
263 let mut gone = false;
266 loop {
267 let now = start.elapsed();
268 let heard = signals.take();
269 if heard.resized {
270 engine.dirty = true;
273 }
274 for cause in heard.causes {
275 if cause == Termination::Hangup && !gone && signals.terminal_gone() {
276 gone = true;
277 guard.abandon();
278 }
279 engine.terminate(cause, now);
280 }
281 engine.poll_tasks();
282 engine.run_queued_work();
283 if gone {
284 refuse_handoffs(&mut engine);
285 } else {
286 run_handoffs(terminal, &mut engine, guard, signals);
287 if let Err(error) = draw(terminal, &mut engine, &mut clipboard, start) {
289 hang_up_or(error, signals, guard, &mut gone)?;
290 }
291 }
292 engine.end_when_due(start.elapsed());
293 if engine.quit {
294 return Ok(());
295 }
296 let now = start.elapsed();
297 let mut wait = match (gone, engine.deadline()) {
298 (true, _) | (false, None) => IDLE_WAIT,
300 (false, Some(deadline)) => deadline.saturating_sub(now),
301 };
302 if let Some(deadline) = engine.ending_deadline() {
303 wait = wait.min(deadline.saturating_sub(now));
304 }
305 if engine.pending_tasks > 0 || (!gone && engine.clipboard_reader.is_reading()) {
306 wait = wait.min(TASK_WAIT);
307 }
308 if let Some(deadline) = clipboard.deadline().filter(|_| !gone) {
309 wait = wait.min(deadline.saturating_sub(now));
310 }
311 let held = if gone { None } else { engine.frame_deadline(now) };
314 if let Some(at) = held {
315 wait = wait.min(at.saturating_sub(now));
316 }
317 if (engine.dirty && held.is_none() && !gone) || engine.has_queued_work() {
318 wait = Duration::ZERO;
319 }
320 if gone {
321 signals.wait(wait, false)?;
322 continue;
323 }
324 match read_input(&mut engine, &mut clipboard, signals, start, wait) {
325 Ok(true) => hang_up(signals, guard, &mut gone),
326 Ok(false) => {}
327 Err(error) => hang_up_or(error, signals, guard, &mut gone)?,
328 }
329 }
330}
331
332fn draw<A: App>(
336 terminal: &mut Terminal<CrosstermBackend<Stdout>>,
337 engine: &mut Engine<A>,
338 clipboard: &mut TerminalClipboard,
339 start: Instant,
340) -> io::Result<()> {
341 let now = start.elapsed();
342 clipboard.update(engine, now)?;
343 engine.tick(now);
344 if engine.frame_due(now) {
345 execute!(io::stdout(), BeginSynchronizedUpdate)?;
346 terminal.draw(|frame| engine.render(frame.buffer_mut(), start.elapsed()))?;
347 execute!(io::stdout(), EndSynchronizedUpdate)?;
348 for text in engine.clipboard.drain(..) {
349 execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))?;
350 }
351 }
352 Ok(())
353}
354
355fn read_input<A: App>(
358 engine: &mut Engine<A>,
359 clipboard: &mut TerminalClipboard,
360 signals: &Signals,
361 start: Instant,
362 wait: Duration,
363) -> io::Result<bool> {
364 let Some(mut ready) = event_waiting(signals)? else {
367 return Ok(true);
368 };
369 if !ready && !wait.is_zero() {
370 let woken = signals.wait(wait, true)?;
371 if woken.hung_up {
372 return Ok(true);
373 }
374 if woken.keyboard {
375 let Some(waiting) = event_waiting(signals)? else {
376 return Ok(true);
377 };
378 ready = waiting;
379 }
380 }
381 while ready {
382 let event = ct::read()?;
383 if let ct::Event::Resize(..) = event {
384 engine.dirty = true;
385 }
386 let more = event_waiting(signals)?;
387 ready = more == Some(true);
388 for event in clipboard.filter(event, ready, engine, start.elapsed()) {
389 if let Some(event) = translate(event) {
390 engine.handle(event, start.elapsed());
391 }
392 }
393 if more.is_none() {
394 return Ok(true);
395 }
396 }
397 Ok(false)
398}
399
400fn event_waiting(signals: &Signals) -> io::Result<Option<bool>> {
404 if signals.hung_up_now() {
405 return Ok(None);
406 }
407 ct::poll(Duration::ZERO).map(Some)
408}
409
410fn hang_up_or(error: io::Error, signals: &Signals, guard: &TerminalGuard, gone: &mut bool) -> io::Result<()> {
413 if !signals.terminal_gone() {
414 return Err(error);
415 }
416 hang_up(signals, guard, gone);
417 Ok(())
418}
419
420fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
423 *gone = true;
424 guard.abandon();
425 signals.hung_up();
426}
427
428fn refuse_handoffs<A: App>(engine: &mut Engine<A>) {
431 const GONE: &str = "the terminal is gone";
432 while let Some(work) = engine.take_handoff() {
433 let message = match work {
434 HandOver::Wait(handoff) => handoff.finish(HandoffOutcome::Failed(GONE.to_owned())),
435 HandOver::Detach(handoff) => handoff.finish(DetachedOutcome::Failed(GONE.to_owned()), engine.deliveries()),
436 };
437 engine.update(message);
438 }
439}
440
441fn run_handoffs<A: App>(
446 terminal: &mut Terminal<CrosstermBackend<Stdout>>,
447 engine: &mut Engine<A>,
448 guard: &TerminalGuard,
449 signals: &Signals,
450) {
451 while let Some(work) = engine.take_handoff() {
452 let prompt = engine.env.i18n().translate("quvyta.handoff.pause", &[]);
453 let deliveries = engine.deliveries();
454 let message = {
455 let mut release = |notice: Option<&str>| -> io::Result<()> {
456 guard.suspend()?;
457 let mut out = io::stdout();
458 execute!(out, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
459 if let Some(text) = notice {
460 writeln!(out, "{text}")?;
461 }
462 out.flush()
463 };
464 let mut take = || -> io::Result<()> {
465 let resumed = guard.resume();
468 let area = terminal.size()?.into();
474 terminal.resize(area)?;
475 resumed
476 };
477 let mut wait_for_key = || wait_for_key_press(&prompt, signals);
478 let mut screen = HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key };
479 signals.handoff(true);
481 let message = match work {
482 HandOver::Wait(handoff) => handoff::run(handoff, &mut screen),
483 HandOver::Detach(handoff) => detached::run(handoff, &mut screen, &deliveries),
484 };
485 signals.handoff(false);
486 message
487 };
488 engine.dirty = true;
489 engine.update(message);
490 }
491}
492
493fn wait_for_key_press(prompt: &str, signals: &Signals) -> io::Result<()> {
495 let mut out = io::stdout();
496 write!(out, "\n{prompt}")?;
497 out.flush()?;
498 enable_raw_mode()?;
500 let pressed = wait_for_key(signals);
501 disable_raw_mode()?;
502 writeln!(out)?;
503 pressed
504}
505
506fn wait_for_key(signals: &Signals) -> io::Result<()> {
509 loop {
510 if signals.pending() {
511 return Ok(());
512 }
513 match event_waiting(signals)? {
514 None => return Ok(()),
516 Some(true) => {
517 if let ct::Event::Key(key) = ct::read()?
518 && key.kind == ct::KeyEventKind::Press
519 {
520 return Ok(());
521 }
522 }
523 Some(false) => {
524 if signals.wait(IDLE_WAIT, true)?.hung_up {
525 return Ok(());
526 }
527 }
528 }
529 }
530}
531
532struct TerminalGuard {
536 keyboard_enhanced: bool,
537 abandoned: Cell<bool>,
540}
541
542impl TerminalGuard {
543 fn enter() -> io::Result<Self> {
544 enable_raw_mode()?;
545 let mut guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
548 execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
549 guard.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
552 guard.push_keyboard_flags()?;
553 Ok(guard)
554 }
555
556 fn suspend(&self) -> io::Result<()> {
558 release(self.keyboard_enhanced)
559 }
560
561 fn resume(&self) -> io::Result<()> {
564 take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
565 }
566
567 fn abandon(&self) {
569 self.abandoned.set(true);
570 }
571
572 fn push_keyboard_flags(&self) -> io::Result<()> {
573 push_keyboard_flags(&mut io::stdout(), self.keyboard_enhanced)
574 }
575}
576
577fn take_back(out: &mut impl Write, keyboard_enhanced: bool, raw_on: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
582 let raw = raw_on();
583 let screen = execute!(out, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide);
584 let flags = push_keyboard_flags(out, keyboard_enhanced);
585 raw.and(screen).and(flags)
586}
587
588fn push_keyboard_flags(out: &mut impl Write, keyboard_enhanced: bool) -> io::Result<()> {
589 if keyboard_enhanced {
590 execute!(
591 out,
592 PushKeyboardEnhancementFlags(
593 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
594 )
595 )?;
596 }
597 Ok(())
598}
599
600impl Drop for TerminalGuard {
601 fn drop(&mut self) {
602 if !self.abandoned.get() {
603 restore(self.keyboard_enhanced);
604 }
605 }
606}
607
608fn release(keyboard_enhanced: bool) -> io::Result<()> {
610 give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
611}
612
613pub(super) fn give_back(
618 out: &mut impl Write,
619 keyboard_enhanced: bool,
620 raw_off: impl FnOnce() -> io::Result<()>,
621) -> io::Result<()> {
622 let flags = if keyboard_enhanced { execute!(out, PopKeyboardEnhancementFlags) } else { Ok(()) };
623 let screen = execute!(out, DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen, cursor::Show);
624 let raw = raw_off();
625 let flushed = out.flush();
626 flags.and(screen).and(raw).and(flushed)
627}
628
629fn restore(keyboard_enhanced: bool) {
631 let _ = release(keyboard_enhanced);
632}
633
634fn install_panic_hook() {
635 on_panic_in_this_thread(|| restore(true));
636}
637
638fn on_panic_in_this_thread(on_panic: impl Fn() + Send + Sync + 'static) {
643 let owner = std::thread::current().id();
644 let previous = std::panic::take_hook();
645 std::panic::set_hook(Box::new(move |info| {
646 if std::thread::current().id() == owner {
647 on_panic();
648 }
649 previous(info);
650 }));
651}
652
653fn translate(event: ct::Event) -> Option<Event> {
655 match event {
656 ct::Event::Key(key) => translate_key(key).map(Event::Key),
657 ct::Event::Mouse(mouse) => translate_mouse(mouse).map(Event::Mouse),
658 ct::Event::Paste(text) => Some(Event::Paste(text)),
659 ct::Event::FocusGained | ct::Event::FocusLost | ct::Event::Resize(..) => None,
660 }
661}
662
663fn modifiers(mods: ct::KeyModifiers) -> Modifiers {
664 Modifiers {
665 ctrl: mods.contains(ct::KeyModifiers::CONTROL),
666 alt: mods.contains(ct::KeyModifiers::ALT),
667 shift: mods.contains(ct::KeyModifiers::SHIFT),
668 }
669}
670
671fn translate_key(key: ct::KeyEvent) -> Option<KeyEvent> {
672 let mut mods = modifiers(key.modifiers);
673 let code = match key.code {
674 ct::KeyCode::Char(' ') => Key::Space,
675 ct::KeyCode::Char(c) if c.is_uppercase() => {
676 mods.shift = true;
677 Key::Char(c.to_lowercase().next().unwrap_or(c))
678 }
679 ct::KeyCode::Char(c) => {
680 if !c.is_alphabetic() {
681 mods.shift = false;
682 }
683 Key::Char(c)
684 }
685 ct::KeyCode::Enter => Key::Enter,
686 ct::KeyCode::Esc => Key::Esc,
687 ct::KeyCode::Tab => Key::Tab,
688 ct::KeyCode::BackTab => {
689 mods.shift = true;
690 Key::Tab
691 }
692 ct::KeyCode::Backspace => Key::Backspace,
693 ct::KeyCode::Delete => Key::Delete,
694 ct::KeyCode::Insert => Key::Insert,
695 ct::KeyCode::Home => Key::Home,
696 ct::KeyCode::End => Key::End,
697 ct::KeyCode::PageUp => Key::PageUp,
698 ct::KeyCode::PageDown => Key::PageDown,
699 ct::KeyCode::Up => Key::Up,
700 ct::KeyCode::Down => Key::Down,
701 ct::KeyCode::Left => Key::Left,
702 ct::KeyCode::Right => Key::Right,
703 ct::KeyCode::F(n) => Key::F(n),
704 ct::KeyCode::Menu => Key::Menu,
705 _ => return None,
706 };
707 let kind = match key.kind {
708 ct::KeyEventKind::Press => KeyKind::Press,
709 ct::KeyEventKind::Repeat => KeyKind::Repeat,
710 ct::KeyEventKind::Release => KeyKind::Release,
711 };
712 let text = match key.code {
713 ct::KeyCode::Char(c) if !mods.ctrl && !mods.alt => Some(c),
714 _ => None,
715 };
716 Some(KeyEvent { chord: KeyChord { key: code, mods }, kind, text })
717}
718
719fn translate_mouse(mouse: ct::MouseEvent) -> Option<MouseEvent> {
720 let button = |b: ct::MouseButton| match b {
721 ct::MouseButton::Left => MouseButton::Left,
722 ct::MouseButton::Right => MouseButton::Right,
723 ct::MouseButton::Middle => MouseButton::Middle,
724 };
725 let kind = match mouse.kind {
726 ct::MouseEventKind::Down(b) => MouseKind::Down(button(b)),
727 ct::MouseEventKind::Up(b) => MouseKind::Up(button(b)),
728 ct::MouseEventKind::Drag(b) => MouseKind::Drag(button(b)),
729 ct::MouseEventKind::Moved => MouseKind::Moved,
730 ct::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
731 ct::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
732 ct::MouseEventKind::ScrollLeft | ct::MouseEventKind::ScrollRight => return None,
733 };
734 Some(MouseEvent { kind, x: i32::from(mouse.column), y: i32::from(mouse.row), mods: modifiers(mouse.modifiers) })
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn panics_on_other_threads_leave_the_terminal_alone() {
743 use std::sync::Arc;
744 use std::sync::atomic::{AtomicUsize, Ordering};
745 let restores = Arc::new(AtomicUsize::new(0));
746 let counter = Arc::clone(&restores);
747 on_panic_in_this_thread(move || {
748 counter.fetch_add(1, Ordering::SeqCst);
749 });
750 let _ = std::thread::spawn(|| panic!("a background task failed")).join();
752 assert_eq!(restores.load(Ordering::SeqCst), 0, "the terminal stays in application mode");
753 let _ = std::panic::catch_unwind(|| panic!("the runtime failed"));
754 assert_eq!(restores.load(Ordering::SeqCst), 1, "a panic of the runtime thread restores it");
755 }
756
757 struct Broken;
759
760 impl Write for Broken {
761 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
762 Err(io::Error::other("the terminal is gone"))
763 }
764
765 fn flush(&mut self) -> io::Result<()> {
766 Err(io::Error::other("the terminal is gone"))
767 }
768 }
769
770 #[test]
771 fn raw_mode_is_left_even_when_the_screen_cannot_be_written() {
772 let mut raw_left = false;
773 let result = give_back(&mut Broken, true, || {
774 raw_left = true;
775 Ok(())
776 });
777 assert!(raw_left, "raw mode is a terminal setting, not output, and is always left");
778 assert_eq!(result.expect_err("the failure is reported").to_string(), "the terminal is gone");
779 }
780
781 #[test]
782 fn leaving_application_mode_writes_every_step_after_one_fails() {
783 let mut out = Vec::new();
784 let result = give_back(&mut out, true, || Err(io::Error::other("no raw mode")));
785 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
786 let text = String::from_utf8(out).expect("escape codes");
787 assert!(text.contains("\x1b[?1049l"), "the alternate screen was left: {text:?}");
788 assert!(text.contains("\x1b[?25h"), "the cursor is shown again: {text:?}");
789 }
790
791 #[test]
792 fn taking_the_terminal_back_goes_on_when_raw_mode_fails() {
793 let mut out = Vec::new();
795 let result = take_back(&mut out, false, || Err(io::Error::other("no raw mode")));
796 assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
797 let text = String::from_utf8(out).expect("escape codes");
798 assert!(text.contains("\x1b[?1049h"), "the alternate screen is entered again: {text:?}");
799 }
800
801 #[test]
802 fn translates_uppercase_and_backtab() {
803 let key = |code, mods| ct::KeyEvent::new(code, mods);
804 let a = translate_key(key(ct::KeyCode::Char('A'), ct::KeyModifiers::SHIFT)).expect("key");
805 assert_eq!(a.chord, "shift+a".parse().expect("chord"));
806 assert_eq!(a.text, Some('A'));
807 let question = translate_key(key(ct::KeyCode::Char('?'), ct::KeyModifiers::SHIFT)).expect("key");
808 assert_eq!(question.chord, "?".parse().expect("chord"));
809 let back = translate_key(key(ct::KeyCode::BackTab, ct::KeyModifiers::SHIFT)).expect("key");
810 assert_eq!(back.chord, "shift+tab".parse().expect("chord"));
811 let ctrl = translate_key(key(ct::KeyCode::Char('q'), ct::KeyModifiers::CONTROL)).expect("key");
812 assert_eq!(ctrl.chord, "ctrl+q".parse().expect("chord"));
813 assert_eq!(ctrl.text, None);
814 let menu = translate_key(key(ct::KeyCode::Menu, ct::KeyModifiers::NONE)).expect("key");
815 assert_eq!(menu.chord, "menu".parse().expect("chord"));
816 }
817}