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    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
35/// How long the loop sleeps when nothing is animating and no background work is running.
36const IDLE_WAIT: Duration = Duration::from_millis(500);
37/// How often finished background work is picked up.
38const TASK_WAIT: Duration = Duration::from_millis(20);
39
40/// Configures and runs an application in the terminal.
41pub 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    /// A runtime for `app` with built-in files only.
51    pub fn new(app: A) -> Self {
52        Self { app, dirs: AssetDirs::default(), theme: None, settings: None, preferences: None }
53    }
54
55    /// Loads theme files from `dir`.
56    #[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    /// Loads a theme file given as text, such as one compiled in with `include_str!`, so an
63    /// installed program needs no files beside it. `file` names it in diagnostics and its stem
64    /// is the theme id, the way a directory names its files. Text given this way wins over
65    /// [`Runtime::theme_dir`].
66    #[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    /// Loads icon set files from `dir`.
73    #[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    /// Loads an icon set given as text, such as one compiled in with `include_str!`, so an
80    /// installed program needs no files beside it. `file` names it in diagnostics and its stem
81    /// is the icon set id, the way a directory names its files. Text given this way wins over
82    /// [`Runtime::icon_dir`].
83    ///
84    /// This is also how an application gives its own icons: keys the built-in set lacks, such as
85    /// `category.internet`, are drawn by every widget that takes an icon key, in whatever set the
86    /// theme chooses and in the glyph mode in use. A key the built-in set has, such as `check`,
87    /// restyles the framework's icon only while a theme names this set; see
88    /// [`IconSetRegistry`](crate::icons::IconSetRegistry).
89    #[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    /// Loads locale files from `dir`.
96    #[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    /// Loads a locale file given as text, such as one compiled in with `include_str!`, so an
103    /// installed program needs no files beside it. `file` names it in diagnostics. Text given
104    /// this way wins over [`Runtime::locale_dir`].
105    ///
106    /// ```no_run
107    /// # use qframe::prelude::*;
108    /// # struct Hello;
109    /// # impl App for Hello {
110    /// #     type Msg = ();
111    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
112    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new(t!("app.greeting"))); }
113    /// # }
114    /// # fn main() -> std::io::Result<()> {
115    /// let english = "[meta]\nname = \"English\"\ncode = \"en\"\n[app]\ngreeting = \"Hello\"\n";
116    /// Runtime::new(Hello).locale_source("en.toml", english).run()
117    /// # }
118    /// ```
119    #[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    /// Layers keymap `file` over the built-in keymap.
126    #[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    /// Layers a keymap given as text over the built-in keymap, such as one compiled in with
133    /// `include_str!`, so an installed program needs no files beside it. `file` names it in
134    /// diagnostics. Text given this way wins over [`Runtime::keymap_file`].
135    ///
136    /// ```no_run
137    /// # use qframe::prelude::*;
138    /// # struct Hello;
139    /// # impl App for Hello {
140    /// #     type Msg = ();
141    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
142    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new("hello")); }
143    /// # }
144    /// # fn main() -> std::io::Result<()> {
145    /// let keys = "[app]\nsave = \"ctrl+s\"\n";
146    /// Runtime::new(Hello).keymap_source("keymap.toml", keys).run()
147    /// # }
148    /// ```
149    #[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    /// Starts with theme `id` instead of the default.
156    #[must_use]
157    pub fn theme(mut self, id: impl Into<String>) -> Self {
158        self.theme = Some(id.into());
159        self
160    }
161
162    /// Starts with the theme, language, icon mode and reduced motion saved in `settings`, so
163    /// the first frame already looks the way the user left it. Saved values win over
164    /// [`Runtime::theme`].
165    #[must_use]
166    pub fn settings(mut self, settings: &Settings) -> Self {
167        self.settings = Some(settings.clone());
168        self
169    }
170
171    /// Starts with the language, theme and icons of the ecosystem's shared
172    /// [`Preferences`], as [`Ecosystem::preferences`](crate::storage::Ecosystem::preferences) resolved
173    /// them for this application. They win over [`Runtime::theme`] and over the same keys in
174    /// [`Runtime::settings`], which keeps the rest: reduced motion, pillar and slide.
175    ///
176    /// ```no_run
177    /// # use qframe::prelude::*;
178    /// # use qframe::i18n::I18n;
179    /// # use qframe::storage::{Ecosystem, Settings};
180    /// # struct Hello;
181    /// # impl App for Hello {
182    /// #     type Msg = ();
183    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
184    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new("hello")); }
185    /// # }
186    /// # fn main() -> std::io::Result<()> {
187    /// let ecosystem = Ecosystem::QUVYTA;
188    /// let settings = Settings::load_member(&ecosystem, "hello");
189    /// let prefs = ecosystem.preferences("hello", &I18n::builtin());
190    /// Runtime::new(Hello).settings(&settings).preferences(&prefs).run()
191    /// # }
192    /// ```
193    #[must_use]
194    pub fn preferences(mut self, preferences: &Preferences) -> Self {
195        self.preferences = Some(preferences.clone());
196        self
197    }
198
199    /// Takes over the terminal and runs until the application quits. The terminal is restored
200    /// on return and on panic.
201    ///
202    /// # Signals
203    ///
204    /// On Unix the run catches `SIGTERM`, `SIGINT` and `SIGHUP` and ends gracefully instead of
205    /// dying on the spot: the application hears the cause through
206    /// [`App::terminating`](super::App::terminating), may save, and quits; see
207    /// [`Termination`] for what each signal does and the grace it leaves. The loop is woken the
208    /// moment a signal arrives, even while it waits for a key or a deadline.
209    ///
210    /// Every way out ends in bounded time: after the grace the run quits without the
211    /// application, a second `SIGTERM` or `SIGINT` quits at once, and when the loop itself is
212    /// stuck the process is ended a second later all the same, by the signal, after the terminal
213    /// is restored. The terminal is left in application mode in no case while it exists; after a
214    /// hangup nothing more is written to it.
215    ///
216    /// During a [`Handoff`](super::Handoff), and a [`DetachedHandoff`](super::DetachedHandoff)
217    /// until the program's first line, the program owns the terminal's foreground. A
218    /// signal the application catches meanwhile is passed on to the program, which ends the way
219    /// it would have as a job of the shell; the application then takes the terminal back and
220    /// hears the signal itself. A hangup reaches the program from the system anyway.
221    ///
222    /// Once `run` returns the signals have their usual effect again.
223    ///
224    /// # Errors
225    ///
226    /// Returns I/O errors from loading asset directories or from the terminal.
227    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        // Before the terminal is taken, so the modes restored if the process has to be ended by
239        // force are the ones the user had.
240        let signals = Signals::catch()?;
241        let mut guard = TerminalGuard::enter()?;
242        // Before anything else reads the terminal: its answers are then read straight from it and
243        // never reach the input parser.
244        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            // A resize arrow left behind would follow the user into the shell. Leaving is under
253            // way whatever happens here, so a failed write only leaves the arrow.
254            let _ = screen.reset_pointer_shape();
255            // Pictures left in the terminal's memory would stay there after the application.
256            #[cfg(feature = "image")]
257            let _ = screen.release_pictures();
258        }
259        let terminal = screen.into_terminal();
260        if guard.abandoned.get() {
261            // Dropping it would show the cursor on a terminal that is gone.
262            std::mem::forget(terminal);
263        } else {
264            drop(terminal);
265        }
266        drop(guard);
267        drop(signals);
268        result
269    }
270}
271
272/// Asks the terminal which pictures it shows and records the answer in `env`, when the answer
273/// could change [`Env::graphics`] and both ends are a terminal. Returns what still watches the
274/// input for an answer that comes too late.
275#[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        // The question may have gone out before the failure; its answer must not become keys.
288        Err(_) => late_from(Instant::now()),
289    }
290}
291
292/// Outside Unix the terminal is not asked, and pictures are drawn with half blocks unless
293/// `QUVYTA_GRAPHICS` says otherwise.
294#[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    // Set once the terminal hung up: from then on nothing is drawn, read or handed over, and the
309    // run only finishes the application's work until it quits.
310    let mut gone = false;
311    loop {
312        let now = start.elapsed();
313        let heard = signals.take();
314        if heard.resized {
315            // The next frame measures the terminal again, even when crossterm's own resize
316            // event has not been read yet.
317            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            // Output to a terminal that just hung up fails before its signal is heard.
333            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            // Frames are not drawn any more, so their deadlines never move.
344            (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        // A frame the frame limit holds back: wake when the gap is over, not with the next
357        // idle wait, so the limit paces frames without adding latency of its own.
358        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
378/// Draws a frame when one is due, after the terminal clipboard and timed input had their turn.
379/// What is due is the engine's answer: a frame the view or an animation wants, unless the frame
380/// limit holds it back; a frame answering input is never held back.
381fn 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
402/// What picks the terminal's own answers out of the input before the engine sees it.
403struct Input<'a> {
404    clipboard: &'a mut TerminalClipboard,
405    late: &'a mut LateAnswer,
406}
407
408/// Waits up to `wait` for the keyboard or a signal and hands every waiting event to the engine.
409/// Returns whether the terminal hung up instead.
410fn 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    // Events crossterm already read ahead come first: the terminal has nothing more to say about
418    // them, so waiting on it would not end.
419    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
455/// Whether crossterm has an event to read, or `None` when the terminal hung up. Crossterm is
456/// asked only while the terminal is there: on a terminal that hung up every read finds nothing,
457/// and its reader would keep reading forever.
458fn 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
465/// Handles a failed exchange with the terminal: when the terminal is gone, it hung up and the
466/// run goes on without it; otherwise the error ends the run.
467fn 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
475/// The terminal hung up: nothing is written to it again, and the application hears a hangup
476/// whether or not its `SIGHUP` arrives.
477fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
478    *gone = true;
479    guard.abandon();
480    signals.hung_up();
481}
482
483/// Answers the handoffs the engine queued after the terminal hung up: there is nothing to hand
484/// over, so each one fails without running its program.
485fn 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
496/// Runs the handoffs the engine queued, oldest first, each one blocking this thread: the screen
497/// is given back, the program runs with the terminal to itself, and afterwards the application
498/// takes the screen and draws all of it again. The engine owns no terminal, so this is the only
499/// place a handoff can happen.
500fn 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        // The program gets the terminal's usual pointer, not the arrow of an edge the pointer
508        // was on. Should the write fail, giving the screen back below fails too and says so.
509        let _ = terminal.reset_pointer_shape();
510        // Nor does it inherit the pictures; they are sent again when the screen comes back.
511        #[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                // Even when a step of taking the terminal back failed, the rest of it happened
527                // and the next frame must be drawn whole, so the failure is reported afterwards.
528                let resumed = guard.resume();
529                // The program wrote over the screen we left, so nothing of it can be reused, and
530                // it may have been resized meanwhile. Resizing to the size the terminal has now
531                // clears it and empties the buffer the next frame is compared against, so every
532                // cell is drawn again. `Terminal::clear` would do the same but first ask the
533                // terminal where its cursor is, a round trip some terminals never answer.
534                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 caught while the program owns the terminal are passed on to it.
541            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
554/// Prints `prompt` on the screen the program leaves behind and waits for one key press.
555fn 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    // The keys are still the terminal's to echo; raw mode makes one press enough.
560    enable_raw_mode()?;
561    let pressed = wait_for_key(signals);
562    disable_raw_mode()?;
563    writeln!(out)?;
564    pressed
565}
566
567/// Waits for one key press, or for a signal that ends the run: nobody should have to press a
568/// key for the application to hear it.
569fn wait_for_key(signals: &Signals) -> io::Result<()> {
570    loop {
571        if signals.pending() {
572            return Ok(());
573        }
574        match event_waiting(signals)? {
575            // Nobody is left to press a key.
576            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
593/// Puts the terminal into application mode and restores it when dropped. The pair of
594/// [`TerminalGuard::suspend`] and [`TerminalGuard::resume`] gives the terminal back for a while,
595/// for a [`Handoff`](super::Handoff), and takes it again with the same keyboard enhancement flags.
596struct TerminalGuard {
597    keyboard_enhanced: bool,
598    /// Set when the terminal hung up: there is nothing left to restore, and nothing is written
599    /// to a terminal that is gone.
600    abandoned: Cell<bool>,
601}
602
603impl TerminalGuard {
604    fn enter() -> io::Result<Self> {
605        enable_raw_mode()?;
606        // From here on the guard exists, so a failure below drops it and the terminal is
607        // restored instead of being left in raw mode.
608        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    /// Asks whether the terminal speaks the kitty keyboard protocol and turns it on if so. Once:
614    /// the terminal cannot change its answer while the application runs, and the question costs a
615    /// round trip to it. The input parser reads the answer, so questions the runtime reads from
616    /// the terminal itself come before this.
617    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    /// Gives the terminal back: raw mode off, the normal screen and the cursor again.
623    fn suspend(&self) -> io::Result<()> {
624        release(self.keyboard_enhanced)
625    }
626
627    /// Takes the terminal again after [`TerminalGuard::suspend`], flags and all. The caller
628    /// redraws afterwards, because the screen it left is gone.
629    fn resume(&self) -> io::Result<()> {
630        take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
631    }
632
633    /// Gives up the terminal after it hung up: dropping the guard then writes nothing.
634    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
643/// Takes the terminal again: raw mode through `raw_on`, then the screen, the mouse and the
644/// keyboard flags written to `out`. Every step is tried even when one before it failed, so a
645/// failure leaves the terminal as close to application mode as it can be; the first error is
646/// the one reported.
647fn 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
674/// Leaves application mode, reporting what failed.
675fn release(keyboard_enhanced: bool) -> io::Result<()> {
676    give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
677}
678
679/// Leaves application mode: the keyboard flags, the mouse and the screen written to `out`, and
680/// raw mode through `raw_off`. Every step is tried even when one before it failed: raw mode is a
681/// setting of the terminal device, not output, and output that cannot be written must not leave
682/// the user's shell in raw mode. The first error is the one reported.
683pub(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
695/// Leaves application mode as far as it can, for a drop or a panic: nothing is left to report to.
696fn 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
704/// Runs `on_panic` before the panic hook that was installed, for panics on the calling thread
705/// only. Hooks run for every panic, even one a background task catches and reports as its
706/// outcome; restoring the terminal then would leave the running application on the normal
707/// screen without raw mode.
708fn 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
719/// Converts a crossterm event; events the framework does not use become `None`.
720fn 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        // A task's panic is caught and becomes its outcome; the application keeps running.
817        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    /// Output that cannot be written, as when the terminal went away.
824    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        // Without the alternate screen the application would draw over the shell's own lines.
860        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}