Skip to main content

qframe/runtime/
terminal.rs

1//! Running an application in a real terminal.
2
3use 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
33/// How long the loop sleeps when nothing is animating and no background work is running.
34const IDLE_WAIT: Duration = Duration::from_millis(500);
35/// How often finished background work is picked up.
36const TASK_WAIT: Duration = Duration::from_millis(20);
37
38/// Configures and runs an application in the terminal.
39pub 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    /// A runtime for `app` with built-in files only.
49    pub fn new(app: A) -> Self {
50        Self { app, dirs: AssetDirs::default(), theme: None, settings: None, preferences: None }
51    }
52
53    /// Loads theme files from `dir`.
54    #[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    /// Loads a theme file given as text, such as one compiled in with `include_str!`, so an
61    /// installed program needs no files beside it. `file` names it in diagnostics and its stem
62    /// is the theme id, the way a directory names its files. Text given this way wins over
63    /// [`Runtime::theme_dir`].
64    #[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    /// Loads icon set files from `dir`.
71    #[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    /// Loads an icon set given as text, such as one compiled in with `include_str!`, so an
78    /// installed program needs no files beside it. `file` names it in diagnostics and its stem
79    /// is the icon set id, the way a directory names its files. Text given this way wins over
80    /// [`Runtime::icon_dir`].
81    ///
82    /// This is also how an application gives its own icons: keys the built-in set lacks, such as
83    /// `category.internet`, are drawn by every widget that takes an icon key, in whatever set the
84    /// theme chooses and in the glyph mode in use. A key the built-in set has, such as `check`,
85    /// restyles the framework's icon only while a theme names this set; see
86    /// [`IconSetRegistry`](crate::icons::IconSetRegistry).
87    #[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    /// Loads locale files from `dir`.
94    #[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    /// Loads a locale file given as text, such as one compiled in with `include_str!`, so an
101    /// installed program needs no files beside it. `file` names it in diagnostics. Text given
102    /// this way wins over [`Runtime::locale_dir`].
103    ///
104    /// ```no_run
105    /// # use qframe::prelude::*;
106    /// # struct Hello;
107    /// # impl App for Hello {
108    /// #     type Msg = ();
109    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
110    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new(t!("app.greeting"))); }
111    /// # }
112    /// # fn main() -> std::io::Result<()> {
113    /// let english = "[meta]\nname = \"English\"\ncode = \"en\"\n[app]\ngreeting = \"Hello\"\n";
114    /// Runtime::new(Hello).locale_source("en.toml", english).run()
115    /// # }
116    /// ```
117    #[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    /// Layers keymap `file` over the built-in keymap.
124    #[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    /// Layers a keymap given as text over the built-in keymap, such as one compiled in with
131    /// `include_str!`, so an installed program needs no files beside it. `file` names it in
132    /// diagnostics. Text given this way wins over [`Runtime::keymap_file`].
133    ///
134    /// ```no_run
135    /// # use qframe::prelude::*;
136    /// # struct Hello;
137    /// # impl App for Hello {
138    /// #     type Msg = ();
139    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
140    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new("hello")); }
141    /// # }
142    /// # fn main() -> std::io::Result<()> {
143    /// let keys = "[app]\nsave = \"ctrl+s\"\n";
144    /// Runtime::new(Hello).keymap_source("keymap.toml", keys).run()
145    /// # }
146    /// ```
147    #[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    /// Starts with theme `id` instead of the default.
154    #[must_use]
155    pub fn theme(mut self, id: impl Into<String>) -> Self {
156        self.theme = Some(id.into());
157        self
158    }
159
160    /// Starts with the theme, language, icon mode and reduced motion saved in `settings`, so
161    /// the first frame already looks the way the user left it. Saved values win over
162    /// [`Runtime::theme`].
163    #[must_use]
164    pub fn settings(mut self, settings: &Settings) -> Self {
165        self.settings = Some(settings.clone());
166        self
167    }
168
169    /// Starts with the language, theme and icons of the family's shared
170    /// [`Preferences`], as [`Family::preferences`](crate::storage::Family::preferences) resolved
171    /// them for this application. They win over [`Runtime::theme`] and over the same keys in
172    /// [`Runtime::settings`], which keeps the rest: reduced motion, pillar and slide.
173    ///
174    /// ```no_run
175    /// # use qframe::prelude::*;
176    /// # use qframe::i18n::I18n;
177    /// # use qframe::storage::{Family, Settings};
178    /// # struct Hello;
179    /// # impl App for Hello {
180    /// #     type Msg = ();
181    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
182    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new("hello")); }
183    /// # }
184    /// # fn main() -> std::io::Result<()> {
185    /// let family = Family::QUVYTA;
186    /// let settings = Settings::load_member(&family, "hello");
187    /// let prefs = family.preferences("hello", &I18n::builtin());
188    /// Runtime::new(Hello).settings(&settings).preferences(&prefs).run()
189    /// # }
190    /// ```
191    #[must_use]
192    pub fn preferences(mut self, preferences: &Preferences) -> Self {
193        self.preferences = Some(preferences.clone());
194        self
195    }
196
197    /// Takes over the terminal and runs until the application quits. The terminal is restored
198    /// on return and on panic.
199    ///
200    /// # Signals
201    ///
202    /// On Unix the run catches `SIGTERM`, `SIGINT` and `SIGHUP` and ends gracefully instead of
203    /// dying on the spot: the application hears the cause through
204    /// [`App::terminating`](super::App::terminating), may save, and quits; see
205    /// [`Termination`] for what each signal does and the grace it leaves. The loop is woken the
206    /// moment a signal arrives, even while it waits for a key or a deadline.
207    ///
208    /// Every way out ends in bounded time: after the grace the run quits without the
209    /// application, a second `SIGTERM` or `SIGINT` quits at once, and when the loop itself is
210    /// stuck the process is ended a second later all the same, by the signal, after the terminal
211    /// is restored. The terminal is left in application mode in no case while it exists; after a
212    /// hangup nothing more is written to it.
213    ///
214    /// During a [`Handoff`](super::Handoff), and a [`DetachedHandoff`](super::DetachedHandoff)
215    /// until the program's first line, the program owns the terminal's foreground. A
216    /// signal the application catches meanwhile is passed on to the program, which ends the way
217    /// it would have as a job of the shell; the application then takes the terminal back and
218    /// hears the signal itself. A hangup reaches the program from the system anyway.
219    ///
220    /// Once `run` returns the signals have their usual effect again.
221    ///
222    /// # Errors
223    ///
224    /// Returns I/O errors from loading asset directories or from the terminal.
225    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        // Before the terminal is taken, so the modes restored if the process has to be ended by
237        // force are the ones the user had.
238        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            // Dropping it would show the cursor on a terminal that is gone.
245            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    // Set once the terminal hung up: from then on nothing is drawn, read or handed over, and the
264    // run only finishes the application's work until it quits.
265    let mut gone = false;
266    loop {
267        let now = start.elapsed();
268        let heard = signals.take();
269        if heard.resized {
270            // The next frame measures the terminal again, even when crossterm's own resize
271            // event has not been read yet.
272            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            // Output to a terminal that just hung up fails before its signal is heard.
288            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            // Frames are not drawn any more, so their deadlines never move.
299            (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        // A frame the frame limit holds back: wake when the gap is over, not with the next
312        // idle wait, so the limit paces frames without adding latency of its own.
313        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
332/// Draws a frame when one is due, after the terminal clipboard and timed input had their turn.
333/// What is due is the engine's answer: a frame the view or an animation wants, unless the frame
334/// limit holds it back; a frame answering input is never held back.
335fn 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
355/// Waits up to `wait` for the keyboard or a signal and hands every waiting event to the engine.
356/// Returns whether the terminal hung up instead.
357fn 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    // Events crossterm already read ahead come first: the terminal has nothing more to say about
365    // them, so waiting on it would not end.
366    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
400/// Whether crossterm has an event to read, or `None` when the terminal hung up. Crossterm is
401/// asked only while the terminal is there: on a terminal that hung up every read finds nothing,
402/// and its reader would keep reading forever.
403fn 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
410/// Handles a failed exchange with the terminal: when the terminal is gone, it hung up and the
411/// run goes on without it; otherwise the error ends the run.
412fn 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
420/// The terminal hung up: nothing is written to it again, and the application hears a hangup
421/// whether or not its `SIGHUP` arrives.
422fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
423    *gone = true;
424    guard.abandon();
425    signals.hung_up();
426}
427
428/// Answers the handoffs the engine queued after the terminal hung up: there is nothing to hand
429/// over, so each one fails without running its program.
430fn 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
441/// Runs the handoffs the engine queued, oldest first, each one blocking this thread: the screen
442/// is given back, the program runs with the terminal to itself, and afterwards the application
443/// takes the screen and draws all of it again. The engine owns no terminal, so this is the only
444/// place a handoff can happen.
445fn 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                // Even when a step of taking the terminal back failed, the rest of it happened
466                // and the next frame must be drawn whole, so the failure is reported afterwards.
467                let resumed = guard.resume();
468                // The program wrote over the screen we left, so nothing of it can be reused, and
469                // it may have been resized meanwhile. Resizing to the size the terminal has now
470                // clears it and empties the buffer the next frame is compared against, so every
471                // cell is drawn again. `Terminal::clear` would do the same but first ask the
472                // terminal where its cursor is, a round trip some terminals never answer.
473                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 caught while the program owns the terminal are passed on to it.
480            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
493/// Prints `prompt` on the screen the program leaves behind and waits for one key press.
494fn 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    // The keys are still the terminal's to echo; raw mode makes one press enough.
499    enable_raw_mode()?;
500    let pressed = wait_for_key(signals);
501    disable_raw_mode()?;
502    writeln!(out)?;
503    pressed
504}
505
506/// Waits for one key press, or for a signal that ends the run: nobody should have to press a
507/// key for the application to hear it.
508fn wait_for_key(signals: &Signals) -> io::Result<()> {
509    loop {
510        if signals.pending() {
511            return Ok(());
512        }
513        match event_waiting(signals)? {
514            // Nobody is left to press a key.
515            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
532/// Puts the terminal into application mode and restores it when dropped. The pair of
533/// [`TerminalGuard::suspend`] and [`TerminalGuard::resume`] gives the terminal back for a while,
534/// for a [`Handoff`](super::Handoff), and takes it again with the same keyboard enhancement flags.
535struct TerminalGuard {
536    keyboard_enhanced: bool,
537    /// Set when the terminal hung up: there is nothing left to restore, and nothing is written
538    /// to a terminal that is gone.
539    abandoned: Cell<bool>,
540}
541
542impl TerminalGuard {
543    fn enter() -> io::Result<Self> {
544        enable_raw_mode()?;
545        // From here on the guard exists, so a failure below drops it and the terminal is
546        // restored instead of being left in raw mode.
547        let mut guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
548        execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
549        // Asked once: the terminal cannot change its answer while the application runs, and the
550        // question costs a round trip to it.
551        guard.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
552        guard.push_keyboard_flags()?;
553        Ok(guard)
554    }
555
556    /// Gives the terminal back: raw mode off, the normal screen and the cursor again.
557    fn suspend(&self) -> io::Result<()> {
558        release(self.keyboard_enhanced)
559    }
560
561    /// Takes the terminal again after [`TerminalGuard::suspend`], flags and all. The caller
562    /// redraws afterwards, because the screen it left is gone.
563    fn resume(&self) -> io::Result<()> {
564        take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
565    }
566
567    /// Gives up the terminal after it hung up: dropping the guard then writes nothing.
568    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
577/// Takes the terminal again: raw mode through `raw_on`, then the screen, the mouse and the
578/// keyboard flags written to `out`. Every step is tried even when one before it failed, so a
579/// failure leaves the terminal as close to application mode as it can be; the first error is
580/// the one reported.
581fn 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
608/// Leaves application mode, reporting what failed.
609fn release(keyboard_enhanced: bool) -> io::Result<()> {
610    give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
611}
612
613/// Leaves application mode: the keyboard flags, the mouse and the screen written to `out`, and
614/// raw mode through `raw_off`. Every step is tried even when one before it failed: raw mode is a
615/// setting of the terminal device, not output, and output that cannot be written must not leave
616/// the user's shell in raw mode. The first error is the one reported.
617pub(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
629/// Leaves application mode as far as it can, for a drop or a panic: nothing is left to report to.
630fn 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
638/// Runs `on_panic` before the panic hook that was installed, for panics on the calling thread
639/// only. Hooks run for every panic, even one a background task catches and reports as its
640/// outcome; restoring the terminal then would leave the running application on the normal
641/// screen without raw mode.
642fn 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
653/// Converts a crossterm event; events the framework does not use become `None`.
654fn 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        // A task's panic is caught and becomes its outcome; the application keeps running.
751        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    /// Output that cannot be written, as when the terminal went away.
758    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        // Without the alternate screen the application would draw over the shell's own lines.
794        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}