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 screen = Screen::new(Terminal::new(CrosstermBackend::new(io::stdout()))?).pointer_shapes(shapes);
249        #[cfg(feature = "image")]
250        let screen = screen.measure_cell(cell_pixels);
251        let mut screen = screen;
252        let engine = Engine::new(self.app, env, TaskMode::Threads);
253        let result = event_loop(&mut screen, engine, &guard, &signals, late);
254        if !guard.abandoned.get() {
255            // A resize arrow left behind would follow the user into the shell. Leaving is under
256            // way whatever happens here, so a failed write only leaves the arrow.
257            let _ = screen.reset_pointer_shape();
258            // Pictures left in the terminal's memory would stay there after the application.
259            #[cfg(feature = "image")]
260            let _ = screen.release_pictures();
261        }
262        let terminal = screen.into_terminal();
263        if guard.abandoned.get() {
264            // Dropping it would show the cursor on a terminal that is gone.
265            std::mem::forget(terminal);
266        } else {
267            drop(terminal);
268        }
269        drop(guard);
270        drop(signals);
271        result
272    }
273}
274
275/// Asks the terminal which pictures it shows and records the answer in `env`, when the answer
276/// could change [`Env::graphics`] and both ends are a terminal. Returns what still watches the
277/// input for an answer that comes too late.
278#[cfg(unix)]
279fn ask_graphics(env: &mut Env, signals: &Signals) -> LateAnswer {
280    use super::graphics_probe::{PROBE_WAIT, late_from, probe};
281    use rustix::termios::isatty;
282    if !env.graphics_worth_asking() || !isatty(signals.tty()) || !isatty(io::stdout()) {
283        return LateAnswer::default();
284    }
285    match probe(signals.tty(), &mut io::stdout(), PROBE_WAIT) {
286        Ok(probe) => {
287            env.set_terminal_graphics(probe.graphics);
288            if probe.answered { LateAnswer::default() } else { late_from(Instant::now()) }
289        }
290        // The question may have gone out before the failure; its answer must not become keys.
291        Err(_) => late_from(Instant::now()),
292    }
293}
294
295/// Outside Unix the terminal is not asked, and pictures are drawn with half blocks unless
296/// `QUVYTA_GRAPHICS` says otherwise.
297#[cfg(not(unix))]
298fn ask_graphics(_env: &mut Env, _signals: &Signals) -> LateAnswer {
299    LateAnswer::default()
300}
301
302fn event_loop<A: App>(
303    terminal: &mut Screen<Stdout>,
304    mut engine: Engine<A>,
305    guard: &TerminalGuard,
306    signals: &Signals,
307    mut late: LateAnswer,
308) -> io::Result<()> {
309    let start = Instant::now();
310    let mut clipboard = TerminalClipboard::default();
311    // Set once the terminal hung up: from then on nothing is drawn, read or handed over, and the
312    // run only finishes the application's work until it quits.
313    let mut gone = false;
314    loop {
315        let now = start.elapsed();
316        let heard = signals.take();
317        if heard.resized {
318            // The next frame measures the terminal again, even when crossterm's own resize
319            // event has not been read yet.
320            engine.dirty = true;
321        }
322        for cause in heard.causes {
323            if cause == Termination::Hangup && !gone && signals.terminal_gone() {
324                gone = true;
325                guard.abandon();
326            }
327            engine.terminate(cause, now);
328        }
329        engine.poll_tasks();
330        engine.run_queued_work();
331        if gone {
332            refuse_handoffs(&mut engine);
333        } else {
334            run_handoffs(terminal, &mut engine, guard, signals);
335            // Output to a terminal that just hung up fails before its signal is heard.
336            if let Err(error) = draw(terminal, &mut engine, &mut clipboard, start) {
337                hang_up_or(error, signals, guard, &mut gone)?;
338            }
339        }
340        engine.end_when_due(start.elapsed());
341        if engine.quit {
342            return Ok(());
343        }
344        let now = start.elapsed();
345        let mut wait = match (gone, engine.deadline()) {
346            // Frames are not drawn any more, so their deadlines never move.
347            (true, _) | (false, None) => IDLE_WAIT,
348            (false, Some(deadline)) => deadline.saturating_sub(now),
349        };
350        if let Some(deadline) = engine.ending_deadline() {
351            wait = wait.min(deadline.saturating_sub(now));
352        }
353        if engine.pending_tasks > 0 || (!gone && engine.clipboard_reader.is_reading()) {
354            wait = wait.min(TASK_WAIT);
355        }
356        if let Some(deadline) = clipboard.deadline().filter(|_| !gone) {
357            wait = wait.min(deadline.saturating_sub(now));
358        }
359        // A frame the frame limit holds back: wake when the gap is over, not with the next
360        // idle wait, so the limit paces frames without adding latency of its own.
361        let held = if gone { None } else { engine.frame_deadline(now) };
362        if let Some(at) = held {
363            wait = wait.min(at.saturating_sub(now));
364        }
365        if (engine.dirty && held.is_none() && !gone) || engine.has_queued_work() {
366            wait = Duration::ZERO;
367        }
368        if gone {
369            signals.wait(wait, false)?;
370            continue;
371        }
372        let mut input = Input { clipboard: &mut clipboard, late: &mut late };
373        match read_input(&mut engine, &mut input, signals, start, wait) {
374            Ok(true) => hang_up(signals, guard, &mut gone),
375            Ok(false) => {}
376            Err(error) => hang_up_or(error, signals, guard, &mut gone)?,
377        }
378    }
379}
380
381/// Draws a frame when one is due, after the terminal clipboard and timed input had their turn.
382/// What is due is the engine's answer: a frame the view or an animation wants, unless the frame
383/// limit holds it back; a frame answering a key, a paste, a press or a release is never held back.
384fn draw<A: App>(
385    terminal: &mut Screen<Stdout>,
386    engine: &mut Engine<A>,
387    clipboard: &mut TerminalClipboard,
388    start: Instant,
389) -> io::Result<()> {
390    let now = start.elapsed();
391    clipboard.update(engine, now)?;
392    engine.tick(now);
393    if engine.frame_due(now) {
394        terminal.present(|buffer| {
395            engine.render(buffer, start.elapsed());
396            engine.painted()
397        })?;
398        for text in engine.clipboard.drain(..) {
399            execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))?;
400        }
401    }
402    Ok(())
403}
404
405/// What picks the terminal's own answers out of the input before the engine sees it.
406struct Input<'a> {
407    clipboard: &'a mut TerminalClipboard,
408    late: &'a mut LateAnswer,
409}
410
411/// Waits up to `wait` for the keyboard or a signal and hands every waiting event to the engine.
412/// Returns whether the terminal hung up instead.
413fn read_input<A: App>(
414    engine: &mut Engine<A>,
415    input: &mut Input<'_>,
416    signals: &Signals,
417    start: Instant,
418    wait: Duration,
419) -> io::Result<bool> {
420    // Events crossterm already read ahead come first: the terminal has nothing more to say about
421    // them, so waiting on it would not end.
422    let Some(mut ready) = event_waiting(signals)? else {
423        return Ok(true);
424    };
425    if !ready && !wait.is_zero() {
426        let woken = signals.wait(wait, true)?;
427        if woken.hung_up {
428            return Ok(true);
429        }
430        if woken.keyboard {
431            let Some(waiting) = event_waiting(signals)? else {
432                return Ok(true);
433            };
434            ready = waiting;
435        }
436    }
437    while ready {
438        let event = ct::read()?;
439        if let ct::Event::Resize(..) = event {
440            engine.dirty = true;
441        }
442        let more = event_waiting(signals)?;
443        ready = more == Some(true);
444        for event in input.late.filter(event, ready, Instant::now()) {
445            for event in input.clipboard.filter(event, ready, engine, start.elapsed()) {
446                if let Some(event) = translate(event) {
447                    engine.handle(event, start.elapsed());
448                }
449            }
450        }
451        if input.late.take_kitty() {
452            heard_kitty_late(engine);
453        }
454        if more.is_none() {
455            return Ok(true);
456        }
457    }
458    Ok(false)
459}
460
461/// The size of a cell in pixels, from the window size the terminal reports; `None` where it
462/// reports no pixels, as some terminals and serial lines do. SSH carries the pixels across.
463#[cfg(feature = "image")]
464fn cell_pixels() -> Option<(u16, u16)> {
465    let size = crossterm::terminal::window_size().ok()?;
466    if size.columns == 0 || size.rows == 0 || size.width == 0 || size.height == 0 {
467        return None;
468    }
469    Some((size.width / size.columns, size.height / size.rows))
470}
471
472/// Takes a kitty `OK` that arrived after the probe stopped waiting, as over a slow link: from the
473/// next frame on pictures are drawn the kitty way, and [`App::graphics`] hears it, unless the
474/// environment rules otherwise.
475fn heard_kitty_late<A: App>(engine: &mut Engine<A>) {
476    engine.env.set_terminal_graphics(crate::graphics::Graphics::Kitty);
477    engine.dirty = true;
478}
479
480/// Whether crossterm has an event to read, or `None` when the terminal hung up. Crossterm is
481/// asked only while the terminal is there: on a terminal that hung up every read finds nothing,
482/// and its reader would keep reading forever.
483fn event_waiting(signals: &Signals) -> io::Result<Option<bool>> {
484    if signals.hung_up_now() {
485        return Ok(None);
486    }
487    ct::poll(Duration::ZERO).map(Some)
488}
489
490/// Handles a failed exchange with the terminal: when the terminal is gone, it hung up and the
491/// run goes on without it; otherwise the error ends the run.
492fn hang_up_or(error: io::Error, signals: &Signals, guard: &TerminalGuard, gone: &mut bool) -> io::Result<()> {
493    if !signals.terminal_gone() {
494        return Err(error);
495    }
496    hang_up(signals, guard, gone);
497    Ok(())
498}
499
500/// The terminal hung up: nothing is written to it again, and the application hears a hangup
501/// whether or not its `SIGHUP` arrives.
502fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
503    *gone = true;
504    guard.abandon();
505    signals.hung_up();
506}
507
508/// Answers the handoffs the engine queued after the terminal hung up: there is nothing to hand
509/// over, so each one fails without running its program.
510fn refuse_handoffs<A: App>(engine: &mut Engine<A>) {
511    const GONE: &str = "the terminal is gone";
512    while let Some(work) = engine.take_handoff() {
513        let message = match work {
514            HandOver::Wait(handoff) => handoff.finish(HandoffOutcome::Failed(GONE.to_owned())),
515            HandOver::Detach(handoff) => handoff.finish(DetachedOutcome::Failed(GONE.to_owned()), engine.deliveries()),
516        };
517        engine.update(message);
518    }
519}
520
521/// Runs the handoffs the engine queued, oldest first, each one blocking this thread: the screen
522/// is given back, the program runs with the terminal to itself, and afterwards the application
523/// takes the screen and draws all of it again. The engine owns no terminal, so this is the only
524/// place a handoff can happen.
525fn run_handoffs<A: App>(
526    terminal: &mut Screen<Stdout>,
527    engine: &mut Engine<A>,
528    guard: &TerminalGuard,
529    signals: &Signals,
530) {
531    while let Some(work) = engine.take_handoff() {
532        // The program gets the terminal's usual pointer, not the arrow of an edge the pointer
533        // was on. Should the write fail, giving the screen back below fails too and says so.
534        let _ = terminal.reset_pointer_shape();
535        // Nor does it inherit the pictures; they are sent again when the screen comes back.
536        #[cfg(feature = "image")]
537        let _ = terminal.release_pictures();
538        let prompt = engine.env.i18n().translate("quvyta.handoff.pause", &[]);
539        let deliveries = engine.deliveries();
540        let message = {
541            let mut release = |notice: Option<&str>| -> io::Result<()> {
542                guard.suspend()?;
543                let mut out = io::stdout();
544                execute!(out, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
545                if let Some(text) = notice {
546                    writeln!(out, "{text}")?;
547                }
548                out.flush()
549            };
550            let mut take = || -> io::Result<()> {
551                // Even when a step of taking the terminal back failed, the rest of it happened
552                // and the next frame must be drawn whole, so the failure is reported afterwards.
553                let resumed = guard.resume();
554                // The program wrote over the screen we left, so nothing of it can be reused, and
555                // it may have been resized meanwhile. Resizing to the size the terminal has now
556                // clears it and empties the buffer the next frame is compared against, so every
557                // cell is drawn again. `Terminal::clear` would do the same but first ask the
558                // terminal where its cursor is, a round trip some terminals never answer.
559                let area = terminal.size()?;
560                terminal.redraw_all(area)?;
561                resumed
562            };
563            let mut wait_for_key = || wait_for_key_press(&prompt, signals);
564            let mut screen = HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key };
565            // Signals caught while the program owns the terminal are passed on to it.
566            signals.handoff(true);
567            let message = match work {
568                HandOver::Wait(handoff) => handoff::run(handoff, &mut screen),
569                HandOver::Detach(handoff) => detached::run(handoff, &mut screen, &deliveries),
570            };
571            signals.handoff(false);
572            message
573        };
574        engine.dirty = true;
575        engine.update(message);
576    }
577}
578
579/// Prints `prompt` on the screen the program leaves behind and waits for one key press.
580fn wait_for_key_press(prompt: &str, signals: &Signals) -> io::Result<()> {
581    let mut out = io::stdout();
582    write!(out, "\n{prompt}")?;
583    out.flush()?;
584    // The keys are still the terminal's to echo; raw mode makes one press enough.
585    enable_raw_mode()?;
586    let pressed = wait_for_key(signals);
587    disable_raw_mode()?;
588    writeln!(out)?;
589    pressed
590}
591
592/// Waits for one key press, or for a signal that ends the run: nobody should have to press a
593/// key for the application to hear it.
594fn wait_for_key(signals: &Signals) -> io::Result<()> {
595    loop {
596        if signals.pending() {
597            return Ok(());
598        }
599        match event_waiting(signals)? {
600            // Nobody is left to press a key.
601            None => return Ok(()),
602            Some(true) => {
603                if let ct::Event::Key(key) = ct::read()?
604                    && key.kind == ct::KeyEventKind::Press
605                {
606                    return Ok(());
607                }
608            }
609            Some(false) => {
610                if signals.wait(IDLE_WAIT, true)?.hung_up {
611                    return Ok(());
612                }
613            }
614        }
615    }
616}
617
618/// Puts the terminal into application mode and restores it when dropped. The pair of
619/// [`TerminalGuard::suspend`] and [`TerminalGuard::resume`] gives the terminal back for a while,
620/// for a [`Handoff`](super::Handoff), and takes it again with the same keyboard enhancement flags.
621struct TerminalGuard {
622    keyboard_enhanced: bool,
623    /// Set when the terminal hung up: there is nothing left to restore, and nothing is written
624    /// to a terminal that is gone.
625    abandoned: Cell<bool>,
626}
627
628impl TerminalGuard {
629    fn enter() -> io::Result<Self> {
630        enable_raw_mode()?;
631        // From here on the guard exists, so a failure below drops it and the terminal is
632        // restored instead of being left in raw mode.
633        let guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
634        execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
635        Ok(guard)
636    }
637
638    /// Asks whether the terminal speaks the kitty keyboard protocol and turns it on if so. Once:
639    /// the terminal cannot change its answer while the application runs, and the question costs a
640    /// round trip to it. The input parser reads the answer, so questions the runtime reads from
641    /// the terminal itself come before this.
642    fn enhance_keyboard(&mut self) -> io::Result<()> {
643        self.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
644        self.push_keyboard_flags()
645    }
646
647    /// Gives the terminal back: raw mode off, the normal screen and the cursor again.
648    fn suspend(&self) -> io::Result<()> {
649        release(self.keyboard_enhanced)
650    }
651
652    /// Takes the terminal again after [`TerminalGuard::suspend`], flags and all. The caller
653    /// redraws afterwards, because the screen it left is gone.
654    fn resume(&self) -> io::Result<()> {
655        take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
656    }
657
658    /// Gives up the terminal after it hung up: dropping the guard then writes nothing.
659    fn abandon(&self) {
660        self.abandoned.set(true);
661    }
662
663    fn push_keyboard_flags(&self) -> io::Result<()> {
664        push_keyboard_flags(&mut io::stdout(), self.keyboard_enhanced)
665    }
666}
667
668/// Takes the terminal again: raw mode through `raw_on`, then the screen, the mouse and the
669/// keyboard flags written to `out`. Every step is tried even when one before it failed, so a
670/// failure leaves the terminal as close to application mode as it can be; the first error is
671/// the one reported.
672fn take_back(out: &mut impl Write, keyboard_enhanced: bool, raw_on: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
673    let raw = raw_on();
674    let screen = execute!(out, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide);
675    let flags = push_keyboard_flags(out, keyboard_enhanced);
676    raw.and(screen).and(flags)
677}
678
679fn push_keyboard_flags(out: &mut impl Write, keyboard_enhanced: bool) -> io::Result<()> {
680    if keyboard_enhanced {
681        execute!(
682            out,
683            PushKeyboardEnhancementFlags(
684                KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
685            )
686        )?;
687    }
688    Ok(())
689}
690
691impl Drop for TerminalGuard {
692    fn drop(&mut self) {
693        if !self.abandoned.get() {
694            restore(self.keyboard_enhanced);
695        }
696    }
697}
698
699/// Leaves application mode, reporting what failed.
700fn release(keyboard_enhanced: bool) -> io::Result<()> {
701    give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
702}
703
704/// Leaves application mode: the keyboard flags, the mouse and the screen written to `out`, and
705/// raw mode through `raw_off`. Every step is tried even when one before it failed: raw mode is a
706/// setting of the terminal device, not output, and output that cannot be written must not leave
707/// the user's shell in raw mode. The first error is the one reported.
708pub(super) fn give_back(
709    out: &mut impl Write,
710    keyboard_enhanced: bool,
711    raw_off: impl FnOnce() -> io::Result<()>,
712) -> io::Result<()> {
713    let flags = if keyboard_enhanced { execute!(out, PopKeyboardEnhancementFlags) } else { Ok(()) };
714    let screen = execute!(out, DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen, cursor::Show);
715    let raw = raw_off();
716    let flushed = out.flush();
717    flags.and(screen).and(raw).and(flushed)
718}
719
720/// Leaves application mode as far as it can, for a drop or a panic: nothing is left to report to.
721fn restore(keyboard_enhanced: bool) {
722    let _ = release(keyboard_enhanced);
723}
724
725fn install_panic_hook() {
726    on_panic_in_this_thread(|| restore(true));
727}
728
729/// Runs `on_panic` before the panic hook that was installed, for panics on the calling thread
730/// only. Hooks run for every panic, even one a background task catches and reports as its
731/// outcome; restoring the terminal then would leave the running application on the normal
732/// screen without raw mode.
733fn on_panic_in_this_thread(on_panic: impl Fn() + Send + Sync + 'static) {
734    let owner = std::thread::current().id();
735    let previous = std::panic::take_hook();
736    std::panic::set_hook(Box::new(move |info| {
737        if std::thread::current().id() == owner {
738            on_panic();
739        }
740        previous(info);
741    }));
742}
743
744/// Converts a crossterm event; events the framework does not use become `None`.
745fn translate(event: ct::Event) -> Option<Event> {
746    match event {
747        ct::Event::Key(key) => translate_key(key).map(Event::Key),
748        ct::Event::Mouse(mouse) => translate_mouse(mouse).map(Event::Mouse),
749        ct::Event::Paste(text) => Some(Event::Paste(text)),
750        ct::Event::FocusGained | ct::Event::FocusLost | ct::Event::Resize(..) => None,
751    }
752}
753
754fn modifiers(mods: ct::KeyModifiers) -> Modifiers {
755    Modifiers {
756        ctrl: mods.contains(ct::KeyModifiers::CONTROL),
757        alt: mods.contains(ct::KeyModifiers::ALT),
758        shift: mods.contains(ct::KeyModifiers::SHIFT),
759    }
760}
761
762fn translate_key(key: ct::KeyEvent) -> Option<KeyEvent> {
763    let mut mods = modifiers(key.modifiers);
764    let code = match key.code {
765        ct::KeyCode::Char(' ') => Key::Space,
766        ct::KeyCode::Char(c) if c.is_uppercase() => {
767            mods.shift = true;
768            Key::Char(c.to_lowercase().next().unwrap_or(c))
769        }
770        ct::KeyCode::Char(c) => {
771            if !c.is_alphabetic() {
772                mods.shift = false;
773            }
774            Key::Char(c)
775        }
776        ct::KeyCode::Enter => Key::Enter,
777        ct::KeyCode::Esc => Key::Esc,
778        ct::KeyCode::Tab => Key::Tab,
779        ct::KeyCode::BackTab => {
780            mods.shift = true;
781            Key::Tab
782        }
783        ct::KeyCode::Backspace => Key::Backspace,
784        ct::KeyCode::Delete => Key::Delete,
785        ct::KeyCode::Insert => Key::Insert,
786        ct::KeyCode::Home => Key::Home,
787        ct::KeyCode::End => Key::End,
788        ct::KeyCode::PageUp => Key::PageUp,
789        ct::KeyCode::PageDown => Key::PageDown,
790        ct::KeyCode::Up => Key::Up,
791        ct::KeyCode::Down => Key::Down,
792        ct::KeyCode::Left => Key::Left,
793        ct::KeyCode::Right => Key::Right,
794        ct::KeyCode::F(n) => Key::F(n),
795        ct::KeyCode::Menu => Key::Menu,
796        _ => return None,
797    };
798    let kind = match key.kind {
799        ct::KeyEventKind::Press => KeyKind::Press,
800        ct::KeyEventKind::Repeat => KeyKind::Repeat,
801        ct::KeyEventKind::Release => KeyKind::Release,
802    };
803    let text = match key.code {
804        ct::KeyCode::Char(c) if !mods.ctrl && !mods.alt => Some(c),
805        _ => None,
806    };
807    Some(KeyEvent { chord: KeyChord { key: code, mods }, kind, text })
808}
809
810fn translate_mouse(mouse: ct::MouseEvent) -> Option<MouseEvent> {
811    let button = |b: ct::MouseButton| match b {
812        ct::MouseButton::Left => MouseButton::Left,
813        ct::MouseButton::Right => MouseButton::Right,
814        ct::MouseButton::Middle => MouseButton::Middle,
815    };
816    let kind = match mouse.kind {
817        ct::MouseEventKind::Down(b) => MouseKind::Down(button(b)),
818        ct::MouseEventKind::Up(b) => MouseKind::Up(button(b)),
819        ct::MouseEventKind::Drag(b) => MouseKind::Drag(button(b)),
820        ct::MouseEventKind::Moved => MouseKind::Moved,
821        ct::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
822        ct::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
823        ct::MouseEventKind::ScrollLeft | ct::MouseEventKind::ScrollRight => return None,
824    };
825    Some(MouseEvent { kind, x: i32::from(mouse.column), y: i32::from(mouse.row), mods: modifiers(mouse.modifiers) })
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831
832    /// Keeps every way of drawing pictures it hears.
833    struct Told(Vec<crate::graphics::Graphics>);
834
835    impl App for Told {
836        type Msg = crate::graphics::Graphics;
837        fn update(&mut self, graphics: Self::Msg) -> crate::runtime::Command<Self::Msg> {
838            self.0.push(graphics);
839            crate::runtime::Command::none()
840        }
841        fn view(&self, _ui: &mut crate::widget::View<'_, Self::Msg>) {}
842        fn graphics(&self, graphics: crate::graphics::Graphics) -> Option<Self::Msg> {
843            Some(graphics)
844        }
845    }
846
847    #[test]
848    fn a_late_kitty_answer_turns_pictures_to_kitty_and_the_application_hears_it() {
849        use crate::graphics::Graphics;
850        let mut engine = Engine::new(Told(Vec::new()), Env::builtin(), TaskMode::Inline);
851        let area = ratatui_core::layout::Rect::new(0, 0, 10, 4);
852        let mut buffer = ratatui_core::buffer::Buffer::empty(area);
853        engine.render(&mut buffer, Duration::ZERO);
854        assert_eq!(engine.app.0, [Graphics::HalfBlock], "no answer in time");
855        engine.dirty = false;
856        heard_kitty_late(&mut engine);
857        assert!(engine.dirty, "a frame is due");
858        engine.render(&mut buffer, Duration::from_secs(1));
859        assert_eq!(engine.app.0, [Graphics::HalfBlock, Graphics::Kitty]);
860        assert_eq!(engine.env.graphics(), Graphics::Kitty);
861    }
862
863    #[test]
864    fn panics_on_other_threads_leave_the_terminal_alone() {
865        use std::sync::Arc;
866        use std::sync::atomic::{AtomicUsize, Ordering};
867        let restores = Arc::new(AtomicUsize::new(0));
868        let counter = Arc::clone(&restores);
869        on_panic_in_this_thread(move || {
870            counter.fetch_add(1, Ordering::SeqCst);
871        });
872        // A task's panic is caught and becomes its outcome; the application keeps running.
873        let _ = std::thread::spawn(|| panic!("a background task failed")).join();
874        assert_eq!(restores.load(Ordering::SeqCst), 0, "the terminal stays in application mode");
875        let _ = std::panic::catch_unwind(|| panic!("the runtime failed"));
876        assert_eq!(restores.load(Ordering::SeqCst), 1, "a panic of the runtime thread restores it");
877    }
878
879    /// Output that cannot be written, as when the terminal went away.
880    struct Broken;
881
882    impl Write for Broken {
883        fn write(&mut self, _: &[u8]) -> io::Result<usize> {
884            Err(io::Error::other("the terminal is gone"))
885        }
886
887        fn flush(&mut self) -> io::Result<()> {
888            Err(io::Error::other("the terminal is gone"))
889        }
890    }
891
892    #[test]
893    fn raw_mode_is_left_even_when_the_screen_cannot_be_written() {
894        let mut raw_left = false;
895        let result = give_back(&mut Broken, true, || {
896            raw_left = true;
897            Ok(())
898        });
899        assert!(raw_left, "raw mode is a terminal setting, not output, and is always left");
900        assert_eq!(result.expect_err("the failure is reported").to_string(), "the terminal is gone");
901    }
902
903    #[test]
904    fn leaving_application_mode_writes_every_step_after_one_fails() {
905        let mut out = Vec::new();
906        let result = give_back(&mut out, true, || Err(io::Error::other("no raw mode")));
907        assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
908        let text = String::from_utf8(out).expect("escape codes");
909        assert!(text.contains("\x1b[?1049l"), "the alternate screen was left: {text:?}");
910        assert!(text.contains("\x1b[?25h"), "the cursor is shown again: {text:?}");
911    }
912
913    #[test]
914    fn taking_the_terminal_back_goes_on_when_raw_mode_fails() {
915        // Without the alternate screen the application would draw over the shell's own lines.
916        let mut out = Vec::new();
917        let result = take_back(&mut out, false, || Err(io::Error::other("no raw mode")));
918        assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
919        let text = String::from_utf8(out).expect("escape codes");
920        assert!(text.contains("\x1b[?1049h"), "the alternate screen is entered again: {text:?}");
921    }
922
923    #[test]
924    fn translates_uppercase_and_backtab() {
925        let key = |code, mods| ct::KeyEvent::new(code, mods);
926        let a = translate_key(key(ct::KeyCode::Char('A'), ct::KeyModifiers::SHIFT)).expect("key");
927        assert_eq!(a.chord, "shift+a".parse().expect("chord"));
928        assert_eq!(a.text, Some('A'));
929        let question = translate_key(key(ct::KeyCode::Char('?'), ct::KeyModifiers::SHIFT)).expect("key");
930        assert_eq!(question.chord, "?".parse().expect("chord"));
931        let back = translate_key(key(ct::KeyCode::BackTab, ct::KeyModifiers::SHIFT)).expect("key");
932        assert_eq!(back.chord, "shift+tab".parse().expect("chord"));
933        let ctrl = translate_key(key(ct::KeyCode::Char('q'), ct::KeyModifiers::CONTROL)).expect("key");
934        assert_eq!(ctrl.chord, "ctrl+q".parse().expect("chord"));
935        assert_eq!(ctrl.text, None);
936        let menu = translate_key(key(ct::KeyCode::Menu, ct::KeyModifiers::NONE)).expect("key");
937        assert_eq!(menu.chord, "menu".parse().expect("chord"));
938    }
939}