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::follow::{Member, Start};
25use super::graphics_probe::LateAnswer;
26use super::handoff::{self, HandoffOutcome, HandoffScreen};
27use super::present::{Screen, pointer_shapes_supported};
28use super::signals::Signals;
29use super::terminal_clipboard::TerminalClipboard;
30use super::termination::Termination;
31use crate::env::{AssetDirs, Env};
32use crate::event::{Event, KeyEvent, KeyKind, MouseButton, MouseEvent, MouseKind};
33use crate::keymap::{Key, KeyChord, Modifiers};
34use crate::storage::{Ecosystem, Preferences, Settings};
35
36/// How long the loop sleeps when nothing is animating and no background work is running.
37const IDLE_WAIT: Duration = Duration::from_millis(500);
38/// How often finished background work is picked up.
39const TASK_WAIT: Duration = Duration::from_millis(20);
40
41/// Configures and runs an application in the terminal.
42pub struct Runtime<A: App> {
43    app: A,
44    dirs: AssetDirs,
45    theme: Option<String>,
46    settings: Option<Settings>,
47    preferences: Option<Preferences>,
48    member: Option<Member>,
49}
50
51impl<A: App> Runtime<A> {
52    /// A runtime for `app` with built-in files only.
53    pub fn new(app: A) -> Self {
54        Self { app, dirs: AssetDirs::default(), theme: None, settings: None, preferences: None, member: None }
55    }
56
57    /// Loads theme files from `dir`.
58    #[must_use]
59    pub fn theme_dir(mut self, dir: impl Into<PathBuf>) -> Self {
60        self.dirs.themes = Some(dir.into());
61        self
62    }
63
64    /// Loads a theme file given as text, such as one compiled in with `include_str!`, so an
65    /// installed program needs no files beside it. `file` names it in diagnostics and its stem
66    /// is the theme id, the way a directory names its files. Text given this way wins over
67    /// [`Runtime::theme_dir`].
68    #[must_use]
69    pub fn theme_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
70        self.dirs.theme_sources.push((file.into(), text.into()));
71        self
72    }
73
74    /// Loads icon set files from `dir`.
75    #[must_use]
76    pub fn icon_dir(mut self, dir: impl Into<PathBuf>) -> Self {
77        self.dirs.icons = Some(dir.into());
78        self
79    }
80
81    /// Loads an icon set given as text, such as one compiled in with `include_str!`, so an
82    /// installed program needs no files beside it. `file` names it in diagnostics and its stem
83    /// is the icon set id, the way a directory names its files. Text given this way wins over
84    /// [`Runtime::icon_dir`].
85    ///
86    /// This is also how an application gives its own icons: keys the built-in set lacks, such as
87    /// `category.internet`, are drawn by every widget that takes an icon key, in whatever set the
88    /// theme chooses and in the glyph mode in use. A key the built-in set has, such as `check`,
89    /// restyles the framework's icon only while a theme names this set; see
90    /// [`IconSetRegistry`](crate::icons::IconSetRegistry).
91    #[must_use]
92    pub fn icon_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
93        self.dirs.icon_sources.push((file.into(), text.into()));
94        self
95    }
96
97    /// Loads locale files from `dir`.
98    #[must_use]
99    pub fn locale_dir(mut self, dir: impl Into<PathBuf>) -> Self {
100        self.dirs.locales = Some(dir.into());
101        self
102    }
103
104    /// Loads a locale file given as text, such as one compiled in with `include_str!`, so an
105    /// installed program needs no files beside it. `file` names it in diagnostics. Text given
106    /// this way wins over [`Runtime::locale_dir`].
107    ///
108    /// ```no_run
109    /// # use qframe::prelude::*;
110    /// # struct Hello;
111    /// # impl App for Hello {
112    /// #     type Msg = ();
113    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
114    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new(t!("app.greeting"))); }
115    /// # }
116    /// # fn main() -> std::io::Result<()> {
117    /// let english = "[meta]\nname = \"English\"\ncode = \"en\"\n[app]\ngreeting = \"Hello\"\n";
118    /// Runtime::new(Hello).locale_source("en.toml", english).run()
119    /// # }
120    /// ```
121    #[must_use]
122    pub fn locale_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
123        self.dirs.locale_sources.push((file.into(), text.into()));
124        self
125    }
126
127    /// Layers keymap `file` over the built-in keymap.
128    #[must_use]
129    pub fn keymap_file(mut self, file: impl Into<PathBuf>) -> Self {
130        self.dirs.keymap = Some(file.into());
131        self
132    }
133
134    /// Layers a keymap given as text over the built-in keymap, such as one compiled in with
135    /// `include_str!`, so an installed program needs no files beside it. `file` names it in
136    /// diagnostics. Text given this way wins over [`Runtime::keymap_file`].
137    ///
138    /// ```no_run
139    /// # use qframe::prelude::*;
140    /// # struct Hello;
141    /// # impl App for Hello {
142    /// #     type Msg = ();
143    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
144    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new("hello")); }
145    /// # }
146    /// # fn main() -> std::io::Result<()> {
147    /// let keys = "[app]\nsave = \"ctrl+s\"\n";
148    /// Runtime::new(Hello).keymap_source("keymap.toml", keys).run()
149    /// # }
150    /// ```
151    #[must_use]
152    pub fn keymap_source(mut self, file: impl Into<String>, text: impl Into<String>) -> Self {
153        self.dirs.keymap_source = Some((file.into(), text.into()));
154        self
155    }
156
157    /// Starts with theme `id` instead of the default.
158    #[must_use]
159    pub fn theme(mut self, id: impl Into<String>) -> Self {
160        self.theme = Some(id.into());
161        self
162    }
163
164    /// Starts with the theme, language, icon mode and reduced motion saved in `settings`, so
165    /// the first frame already looks the way the user left it. Saved values win over
166    /// [`Runtime::theme`].
167    #[must_use]
168    pub fn settings(mut self, settings: &Settings) -> Self {
169        self.settings = Some(settings.clone());
170        self
171    }
172
173    /// Starts with the language, theme, icons and reduced motion of the ecosystem's shared
174    /// [`Preferences`], as [`Ecosystem::preferences`](crate::storage::Ecosystem::preferences) resolved
175    /// them for this application. They win over [`Runtime::theme`] and over the same keys in
176    /// [`Runtime::settings`], which keeps the rest: pillar and slide.
177    ///
178    /// ```no_run
179    /// # use qframe::prelude::*;
180    /// # use qframe::i18n::I18n;
181    /// # use qframe::storage::{Ecosystem, Settings};
182    /// # struct Hello;
183    /// # impl App for Hello {
184    /// #     type Msg = ();
185    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
186    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new("hello")); }
187    /// # }
188    /// # fn main() -> std::io::Result<()> {
189    /// let ecosystem = Ecosystem::QUVYTA;
190    /// let settings = Settings::load_member(&ecosystem, "hello");
191    /// let prefs = ecosystem.preferences("hello", &I18n::builtin());
192    /// Runtime::new(Hello).settings(&settings).preferences(&prefs).run()
193    /// # }
194    /// ```
195    #[must_use]
196    pub fn preferences(mut self, preferences: &Preferences) -> Self {
197        self.preferences = Some(preferences.clone());
198        self
199    }
200
201    /// Runs the application as member `app` of `ecosystem`, in one call: its own settings are
202    /// loaded with [`Settings::load_member`], the shared preferences resolved with
203    /// [`Ecosystem::preferences`] in the language files this runtime loads, and both applied
204    /// before the first frame, as [`Runtime::settings`] and [`Runtime::preferences`] apply them.
205    ///
206    /// While the application runs, the ecosystem's folder is watched. When the shared file or the
207    /// application's own file changes, say because another application switched the theme for
208    /// the whole ecosystem, the preferences are resolved again without writing anything and what
209    /// changed is applied at once: language, theme, icons and reduced motion, and the pillar
210    /// when the application's own file changed it. A value the application chose for itself
211    /// stays, since resolving gives it first. [`App::preferences`](super::App::preferences) hears
212    /// the start and every change, so a settings screen can show the new values.
213    ///
214    /// The watch uses the system's own events and a thread that sleeps until the folder changes;
215    /// it costs nothing while nothing changes. Where the folder cannot be watched (no home
216    /// folder, a platform without a folder watch, the system's limit on watches reached), the
217    /// application runs with what it started with, as before. The watch ends with the run.
218    ///
219    /// Settings or preferences also given with [`Runtime::settings`] or [`Runtime::preferences`]
220    /// are used as given and not read a second time, so an application moving over can keep
221    /// its own reading for now.
222    ///
223    /// ```no_run
224    /// # use qframe::prelude::*;
225    /// # use qframe::storage::Ecosystem;
226    /// # struct Hello;
227    /// # impl App for Hello {
228    /// #     type Msg = ();
229    /// #     fn update(&mut self, _: ()) -> Command<()> { Command::none() }
230    /// #     fn view(&self, ui: &mut View<'_, ()>) { ui.add(Text::new("hello")); }
231    /// # }
232    /// # fn main() -> std::io::Result<()> {
233    /// Runtime::new(Hello).member(Ecosystem::QUVYTA, "hello").run()
234    /// # }
235    /// ```
236    #[must_use]
237    pub fn member(mut self, ecosystem: Ecosystem, app: &str) -> Self {
238        self.member = Some(Member::new(ecosystem, app, None));
239        self
240    }
241
242    /// [`member`](Self::member) with `config_dir` as the ecosystem's folder instead of this
243    /// platform's, for an application that keeps its settings where it chooses, and for a demo.
244    #[must_use]
245    pub fn member_in(mut self, ecosystem: Ecosystem, config_dir: impl Into<PathBuf>, app: &str) -> Self {
246        self.member = Some(Member::new(ecosystem, app, Some(config_dir.into())));
247        self
248    }
249
250    /// Takes over the terminal and runs until the application quits. The terminal is restored
251    /// on return and on panic.
252    ///
253    /// # Signals
254    ///
255    /// On Unix the run catches `SIGTERM`, `SIGINT` and `SIGHUP` and ends gracefully instead of
256    /// dying on the spot: the application hears the cause through
257    /// [`App::terminating`](super::App::terminating), may save, and quits; see
258    /// [`Termination`] for what each signal does and the grace it leaves. The loop is woken the
259    /// moment a signal arrives, even while it waits for a key or a deadline.
260    ///
261    /// Every way out ends in bounded time: after the grace the run quits without the
262    /// application, a second `SIGTERM` or `SIGINT` quits at once, and when the loop itself is
263    /// stuck the process is ended a second later all the same, by the signal, after the terminal
264    /// is restored. The terminal is left in application mode in no case while it exists; after a
265    /// hangup nothing more is written to it.
266    ///
267    /// During a [`Handoff`](super::Handoff), and a [`DetachedHandoff`](super::DetachedHandoff)
268    /// until the program's first line, the program owns the terminal's foreground. A
269    /// signal the application catches meanwhile is passed on to the program, which ends the way
270    /// it would have as a job of the shell; the application then takes the terminal back and
271    /// hears the signal itself. A hangup reaches the program from the system anyway.
272    ///
273    /// Once `run` returns the signals have their usual effect again.
274    ///
275    /// # Errors
276    ///
277    /// Returns I/O errors from loading asset directories or from the terminal.
278    pub fn run(self) -> io::Result<()> {
279        let mut env = Env::load(&self.dirs)?;
280        let start = Start { theme: self.theme.as_deref(), settings: self.settings, preferences: self.preferences };
281        let follow = start.apply(&mut env, self.member, true);
282        // Before the terminal is taken, so the modes restored if the process has to be ended by
283        // force are the ones the user had.
284        let signals = Signals::catch()?;
285        let mut guard = TerminalGuard::enter()?;
286        // Before anything else reads the terminal: its answers are then read straight from it and
287        // never reach the input parser.
288        let late = ask_graphics(&mut env, &signals);
289        guard.enhance_keyboard()?;
290        install_panic_hook();
291        let shapes = pointer_shapes_supported(|name| std::env::var(name).ok());
292        let mut screen = Screen::new(Terminal::new(CrosstermBackend::new(io::stdout()))?).pointer_shapes(shapes);
293        env.set_cell_pixels(cell_pixels());
294        let mut engine = Engine::new(self.app, env, TaskMode::Threads);
295        if let Some(follow) = follow {
296            engine.follow(follow);
297        }
298        let result = event_loop(&mut screen, engine, &guard, &signals, late);
299        if !guard.abandoned.get() {
300            // A resize arrow left behind would follow the user into the shell. Leaving is under
301            // way whatever happens here, so a failed write only leaves the arrow.
302            let _ = screen.reset_pointer_shape();
303            // Pictures left in the terminal's memory would stay there after the application.
304            #[cfg(feature = "image")]
305            let _ = screen.release_pictures();
306        }
307        let terminal = screen.into_terminal();
308        if guard.abandoned.get() {
309            // Dropping it would show the cursor on a terminal that is gone.
310            std::mem::forget(terminal);
311        } else {
312            drop(terminal);
313        }
314        drop(guard);
315        drop(signals);
316        result
317    }
318}
319
320/// Asks the terminal which pictures it shows and records the answer in `env`, when the answer
321/// could change [`Env::graphics`] and both ends are a terminal. Returns what still watches the
322/// input for an answer that comes too late.
323#[cfg(unix)]
324fn ask_graphics(env: &mut Env, signals: &Signals) -> LateAnswer {
325    use super::graphics_probe::{PROBE_WAIT, late_from, probe};
326    use rustix::termios::isatty;
327    if !env.graphics_worth_asking() || !isatty(signals.tty()) || !isatty(io::stdout()) {
328        return LateAnswer::default();
329    }
330    match probe(signals.tty(), &mut io::stdout(), PROBE_WAIT) {
331        Ok(probe) => {
332            env.set_terminal_graphics(probe.graphics);
333            if probe.answered { LateAnswer::default() } else { late_from(Instant::now()) }
334        }
335        // The question may have gone out before the failure; its answer must not become keys.
336        Err(_) => late_from(Instant::now()),
337    }
338}
339
340/// Outside Unix the terminal is not asked, and pictures are drawn with half blocks unless
341/// `QUVYTA_GRAPHICS` says otherwise.
342#[cfg(not(unix))]
343fn ask_graphics(_env: &mut Env, _signals: &Signals) -> LateAnswer {
344    LateAnswer::default()
345}
346
347fn event_loop<A: App>(
348    terminal: &mut Screen<Stdout>,
349    mut engine: Engine<A>,
350    guard: &TerminalGuard,
351    signals: &Signals,
352    mut late: LateAnswer,
353) -> io::Result<()> {
354    let start = Instant::now();
355    let mut clipboard = TerminalClipboard::default();
356    // Set once the terminal hung up: from then on nothing is drawn, read or handed over, and the
357    // run only finishes the application's work until it quits.
358    let mut gone = false;
359    loop {
360        let now = start.elapsed();
361        let heard = signals.take();
362        if heard.resized {
363            // The next frame measures the terminal again, even when crossterm's own resize
364            // event has not been read yet.
365            engine.dirty = true;
366        }
367        for cause in heard.causes {
368            if cause == Termination::Hangup && !gone && signals.terminal_gone() {
369                gone = true;
370                guard.abandon();
371            }
372            engine.terminate(cause, now);
373        }
374        engine.poll_tasks();
375        engine.run_queued_work();
376        engine.follow_preferences();
377        if gone {
378            refuse_handoffs(&mut engine);
379        } else {
380            run_handoffs(terminal, &mut engine, guard, signals);
381            // Output to a terminal that just hung up fails before its signal is heard.
382            if let Err(error) = draw(terminal, &mut engine, &mut clipboard, start) {
383                hang_up_or(error, signals, guard, &mut gone)?;
384            }
385        }
386        engine.end_when_due(start.elapsed());
387        if engine.quit {
388            return Ok(());
389        }
390        let now = start.elapsed();
391        let mut wait = match (gone, engine.deadline()) {
392            // Frames are not drawn any more, so their deadlines never move.
393            (true, _) | (false, None) => IDLE_WAIT,
394            (false, Some(deadline)) => deadline.saturating_sub(now),
395        };
396        if let Some(deadline) = engine.ending_deadline() {
397            wait = wait.min(deadline.saturating_sub(now));
398        }
399        if engine.pending_tasks > 0 || (!gone && engine.clipboard_reader.is_reading()) {
400            wait = wait.min(TASK_WAIT);
401        }
402        if let Some(deadline) = clipboard.deadline().filter(|_| !gone) {
403            wait = wait.min(deadline.saturating_sub(now));
404        }
405        // A frame the frame limit holds back: wake when the gap is over, not with the next
406        // idle wait, so the limit paces frames without adding latency of its own.
407        let held = if gone { None } else { engine.frame_deadline(now) };
408        if let Some(at) = held {
409            wait = wait.min(at.saturating_sub(now));
410        }
411        if (engine.dirty && held.is_none() && !gone) || engine.has_queued_work() {
412            wait = Duration::ZERO;
413        }
414        if gone {
415            signals.wait(wait, false)?;
416            continue;
417        }
418        let mut input = Input { clipboard: &mut clipboard, late: &mut late };
419        match read_input(&mut engine, &mut input, signals, start, wait) {
420            Ok(true) => hang_up(signals, guard, &mut gone),
421            Ok(false) => {}
422            Err(error) => hang_up_or(error, signals, guard, &mut gone)?,
423        }
424    }
425}
426
427/// Draws a frame when one is due, after the terminal clipboard and timed input had their turn.
428/// What is due is the engine's answer: a frame the view or an animation wants, unless the frame
429/// limit holds it back; a frame answering a key, a paste, a press or a release is never held back.
430fn draw<A: App>(
431    terminal: &mut Screen<Stdout>,
432    engine: &mut Engine<A>,
433    clipboard: &mut TerminalClipboard,
434    start: Instant,
435) -> io::Result<()> {
436    let now = start.elapsed();
437    clipboard.update(engine, now)?;
438    engine.tick(now);
439    if engine.frame_due(now) {
440        terminal.present(|buffer| {
441            engine.render(buffer, start.elapsed());
442            engine.painted()
443        })?;
444        for text in engine.clipboard.drain(..) {
445            execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))?;
446        }
447    }
448    Ok(())
449}
450
451/// What picks the terminal's own answers out of the input before the engine sees it.
452struct Input<'a> {
453    clipboard: &'a mut TerminalClipboard,
454    late: &'a mut LateAnswer,
455}
456
457/// Waits up to `wait` for the keyboard or a signal and hands every waiting event to the engine.
458/// Returns whether the terminal hung up instead.
459fn read_input<A: App>(
460    engine: &mut Engine<A>,
461    input: &mut Input<'_>,
462    signals: &Signals,
463    start: Instant,
464    wait: Duration,
465) -> io::Result<bool> {
466    // Events crossterm already read ahead come first: the terminal has nothing more to say about
467    // them, so waiting on it would not end.
468    let Some(mut ready) = event_waiting(signals)? else {
469        return Ok(true);
470    };
471    if !ready && !wait.is_zero() {
472        let woken = signals.wait(wait, true)?;
473        if woken.hung_up {
474            return Ok(true);
475        }
476        if woken.keyboard {
477            let Some(waiting) = event_waiting(signals)? else {
478                return Ok(true);
479            };
480            ready = waiting;
481        }
482    }
483    while ready {
484        let event = ct::read()?;
485        if let ct::Event::Resize(..) = event {
486            engine.dirty = true;
487            // A font size change is a resize too, with the same columns and rows.
488            took_cell(engine, cell_pixels());
489        }
490        let more = event_waiting(signals)?;
491        ready = more == Some(true);
492        for event in input.late.filter(event, ready, Instant::now()) {
493            for event in input.clipboard.filter(event, ready, engine, start.elapsed()) {
494                if let Some(event) = translate(event) {
495                    engine.handle(event, start.elapsed());
496                }
497            }
498        }
499        if input.late.take_kitty() {
500            heard_kitty_late(engine);
501        }
502        if more.is_none() {
503            return Ok(true);
504        }
505    }
506    Ok(false)
507}
508
509/// The size of a cell in pixels, from the window size the terminal reports; `None` where it
510/// reports no pixels, as some terminals and serial lines do. SSH carries the pixels across.
511fn cell_pixels() -> Option<(u16, u16)> {
512    let size = crossterm::terminal::window_size().ok()?;
513    cell_of(size.columns, size.rows, size.width, size.height)
514}
515
516/// A cell of a window `width` × `height` pixels across `columns` × `rows` cells; `None` when
517/// any of them is zero or the pixels are fewer than the cells, which is no report at all.
518fn cell_of(columns: u16, rows: u16, width: u16, height: u16) -> Option<(u16, u16)> {
519    let cell = (width.checked_div(columns)?, height.checked_div(rows)?);
520    (cell.0 > 0 && cell.1 > 0).then_some(cell)
521}
522
523/// Takes the size of a cell the terminal reports now, and draws a frame when it changed, so the
524/// view sees the new one.
525fn took_cell<A: App>(engine: &mut Engine<A>, cell: Option<(u16, u16)>) {
526    if engine.env.cell_pixels() != cell {
527        engine.env.set_cell_pixels(cell);
528        engine.dirty = true;
529    }
530}
531
532/// Takes a kitty `OK` that arrived after the probe stopped waiting, as over a slow link: from the
533/// next frame on pictures are drawn the kitty way, and [`App::graphics`] hears it, unless the
534/// environment rules otherwise.
535fn heard_kitty_late<A: App>(engine: &mut Engine<A>) {
536    engine.env.set_terminal_graphics(crate::graphics::Graphics::Kitty);
537    engine.dirty = true;
538}
539
540/// Whether crossterm has an event to read, or `None` when the terminal hung up. Crossterm is
541/// asked only while the terminal is there: on a terminal that hung up every read finds nothing,
542/// and its reader would keep reading forever.
543fn event_waiting(signals: &Signals) -> io::Result<Option<bool>> {
544    if signals.hung_up_now() {
545        return Ok(None);
546    }
547    ct::poll(Duration::ZERO).map(Some)
548}
549
550/// Handles a failed exchange with the terminal: when the terminal is gone, it hung up and the
551/// run goes on without it; otherwise the error ends the run.
552fn hang_up_or(error: io::Error, signals: &Signals, guard: &TerminalGuard, gone: &mut bool) -> io::Result<()> {
553    if !signals.terminal_gone() {
554        return Err(error);
555    }
556    hang_up(signals, guard, gone);
557    Ok(())
558}
559
560/// The terminal hung up: nothing is written to it again, and the application hears a hangup
561/// whether or not its `SIGHUP` arrives.
562fn hang_up(signals: &Signals, guard: &TerminalGuard, gone: &mut bool) {
563    *gone = true;
564    guard.abandon();
565    signals.hung_up();
566}
567
568/// Answers the handoffs the engine queued after the terminal hung up: there is nothing to hand
569/// over, so each one fails without running its program.
570fn refuse_handoffs<A: App>(engine: &mut Engine<A>) {
571    const GONE: &str = "the terminal is gone";
572    while let Some(work) = engine.take_handoff() {
573        let message = match work {
574            HandOver::Wait(handoff) => handoff.finish(HandoffOutcome::Failed(GONE.to_owned())),
575            HandOver::Detach(handoff) => handoff.finish(DetachedOutcome::Failed(GONE.to_owned()), engine.deliveries()),
576        };
577        engine.update(message);
578    }
579}
580
581/// Runs the handoffs the engine queued, oldest first, each one blocking this thread: the screen
582/// is given back, the program runs with the terminal to itself, and afterwards the application
583/// takes the screen and draws all of it again. The engine owns no terminal, so this is the only
584/// place a handoff can happen.
585fn run_handoffs<A: App>(
586    terminal: &mut Screen<Stdout>,
587    engine: &mut Engine<A>,
588    guard: &TerminalGuard,
589    signals: &Signals,
590) {
591    while let Some(work) = engine.take_handoff() {
592        // The program gets the terminal's usual pointer, not the arrow of an edge the pointer
593        // was on. Should the write fail, giving the screen back below fails too and says so.
594        let _ = terminal.reset_pointer_shape();
595        // Nor does it inherit the pictures; they are sent again when the screen comes back.
596        #[cfg(feature = "image")]
597        let _ = terminal.release_pictures();
598        let prompt = engine.env.i18n().translate("quvyta.handoff.pause", &[]);
599        let deliveries = engine.deliveries();
600        let message = {
601            let mut release = |notice: Option<&str>| -> io::Result<()> {
602                guard.suspend()?;
603                let mut out = io::stdout();
604                execute!(out, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
605                if let Some(text) = notice {
606                    writeln!(out, "{text}")?;
607                }
608                out.flush()
609            };
610            let mut take = || -> io::Result<()> {
611                // Even when a step of taking the terminal back failed, the rest of it happened
612                // and the next frame must be drawn whole, so the failure is reported afterwards.
613                let resumed = guard.resume();
614                // The program wrote over the screen we left, so nothing of it can be reused, and
615                // it may have been resized meanwhile. Resizing to the size the terminal has now
616                // clears it and empties the buffer the next frame is compared against, so every
617                // cell is drawn again. `Terminal::clear` would do the same but first ask the
618                // terminal where its cursor is, a round trip some terminals never answer.
619                let area = terminal.size()?;
620                terminal.redraw_all(area)?;
621                resumed
622            };
623            let mut wait_for_key = || wait_for_key_press(&prompt, signals);
624            let mut screen = HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key };
625            // Signals caught while the program owns the terminal are passed on to it.
626            signals.handoff(true);
627            let message = match work {
628                HandOver::Wait(handoff) => handoff::run(handoff, &mut screen),
629                HandOver::Detach(handoff) => detached::run(handoff, &mut screen, &deliveries),
630            };
631            signals.handoff(false);
632            message
633        };
634        engine.dirty = true;
635        engine.update(message);
636    }
637}
638
639/// Prints `prompt` on the screen the program leaves behind and waits for one key press.
640fn wait_for_key_press(prompt: &str, signals: &Signals) -> io::Result<()> {
641    let mut out = io::stdout();
642    write!(out, "\n{prompt}")?;
643    out.flush()?;
644    // The keys are still the terminal's to echo; raw mode makes one press enough.
645    enable_raw_mode()?;
646    let pressed = wait_for_key(signals);
647    disable_raw_mode()?;
648    writeln!(out)?;
649    pressed
650}
651
652/// Waits for one key press, or for a signal that ends the run: nobody should have to press a
653/// key for the application to hear it.
654fn wait_for_key(signals: &Signals) -> io::Result<()> {
655    loop {
656        if signals.pending() {
657            return Ok(());
658        }
659        match event_waiting(signals)? {
660            // Nobody is left to press a key.
661            None => return Ok(()),
662            Some(true) => {
663                if let ct::Event::Key(key) = ct::read()?
664                    && key.kind == ct::KeyEventKind::Press
665                {
666                    return Ok(());
667                }
668            }
669            Some(false) => {
670                if signals.wait(IDLE_WAIT, true)?.hung_up {
671                    return Ok(());
672                }
673            }
674        }
675    }
676}
677
678/// Puts the terminal into application mode and restores it when dropped. The pair of
679/// [`TerminalGuard::suspend`] and [`TerminalGuard::resume`] gives the terminal back for a while,
680/// for a [`Handoff`](super::Handoff), and takes it again with the same keyboard enhancement flags.
681struct TerminalGuard {
682    keyboard_enhanced: bool,
683    /// Set when the terminal hung up: there is nothing left to restore, and nothing is written
684    /// to a terminal that is gone.
685    abandoned: Cell<bool>,
686}
687
688impl TerminalGuard {
689    fn enter() -> io::Result<Self> {
690        enable_raw_mode()?;
691        // From here on the guard exists, so a failure below drops it and the terminal is
692        // restored instead of being left in raw mode.
693        let guard = Self { keyboard_enhanced: false, abandoned: Cell::new(false) };
694        execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide)?;
695        Ok(guard)
696    }
697
698    /// Asks whether the terminal speaks the kitty keyboard protocol and turns it on if so. Once:
699    /// the terminal cannot change its answer while the application runs, and the question costs a
700    /// round trip to it. The input parser reads the answer, so questions the runtime reads from
701    /// the terminal itself come before this.
702    fn enhance_keyboard(&mut self) -> io::Result<()> {
703        self.keyboard_enhanced = supports_keyboard_enhancement().unwrap_or(false);
704        self.push_keyboard_flags()
705    }
706
707    /// Gives the terminal back: raw mode off, the normal screen and the cursor again.
708    fn suspend(&self) -> io::Result<()> {
709        release(self.keyboard_enhanced)
710    }
711
712    /// Takes the terminal again after [`TerminalGuard::suspend`], flags and all. The caller
713    /// redraws afterwards, because the screen it left is gone.
714    fn resume(&self) -> io::Result<()> {
715        take_back(&mut io::stdout(), self.keyboard_enhanced, enable_raw_mode)
716    }
717
718    /// Gives up the terminal after it hung up: dropping the guard then writes nothing.
719    fn abandon(&self) {
720        self.abandoned.set(true);
721    }
722
723    fn push_keyboard_flags(&self) -> io::Result<()> {
724        push_keyboard_flags(&mut io::stdout(), self.keyboard_enhanced)
725    }
726}
727
728/// Takes the terminal again: raw mode through `raw_on`, then the screen, the mouse and the
729/// keyboard flags written to `out`. Every step is tried even when one before it failed, so a
730/// failure leaves the terminal as close to application mode as it can be; the first error is
731/// the one reported.
732fn take_back(out: &mut impl Write, keyboard_enhanced: bool, raw_on: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
733    let raw = raw_on();
734    let screen = execute!(out, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste, cursor::Hide);
735    let flags = push_keyboard_flags(out, keyboard_enhanced);
736    raw.and(screen).and(flags)
737}
738
739fn push_keyboard_flags(out: &mut impl Write, keyboard_enhanced: bool) -> io::Result<()> {
740    if keyboard_enhanced {
741        execute!(
742            out,
743            PushKeyboardEnhancementFlags(
744                KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
745            )
746        )?;
747    }
748    Ok(())
749}
750
751impl Drop for TerminalGuard {
752    fn drop(&mut self) {
753        if !self.abandoned.get() {
754            restore(self.keyboard_enhanced);
755        }
756    }
757}
758
759/// Leaves application mode, reporting what failed.
760fn release(keyboard_enhanced: bool) -> io::Result<()> {
761    give_back(&mut io::stdout(), keyboard_enhanced, disable_raw_mode)
762}
763
764/// Leaves application mode: the keyboard flags, the mouse and the screen written to `out`, and
765/// raw mode through `raw_off`. Every step is tried even when one before it failed: raw mode is a
766/// setting of the terminal device, not output, and output that cannot be written must not leave
767/// the user's shell in raw mode. The first error is the one reported.
768pub(super) fn give_back(
769    out: &mut impl Write,
770    keyboard_enhanced: bool,
771    raw_off: impl FnOnce() -> io::Result<()>,
772) -> io::Result<()> {
773    let flags = if keyboard_enhanced { execute!(out, PopKeyboardEnhancementFlags) } else { Ok(()) };
774    let screen = execute!(out, DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen, cursor::Show);
775    let raw = raw_off();
776    let flushed = out.flush();
777    flags.and(screen).and(raw).and(flushed)
778}
779
780/// Leaves application mode as far as it can, for a drop or a panic: nothing is left to report to.
781fn restore(keyboard_enhanced: bool) {
782    let _ = release(keyboard_enhanced);
783}
784
785fn install_panic_hook() {
786    on_panic_in_this_thread(|| restore(true));
787}
788
789/// Runs `on_panic` before the panic hook that was installed, for panics on the calling thread
790/// only. Hooks run for every panic, even one a background task catches and reports as its
791/// outcome; restoring the terminal then would leave the running application on the normal
792/// screen without raw mode.
793fn on_panic_in_this_thread(on_panic: impl Fn() + Send + Sync + 'static) {
794    let owner = std::thread::current().id();
795    let previous = std::panic::take_hook();
796    std::panic::set_hook(Box::new(move |info| {
797        if std::thread::current().id() == owner {
798            on_panic();
799        }
800        previous(info);
801    }));
802}
803
804/// Converts a crossterm event; events the framework does not use become `None`.
805fn translate(event: ct::Event) -> Option<Event> {
806    match event {
807        ct::Event::Key(key) => translate_key(key).map(Event::Key),
808        ct::Event::Mouse(mouse) => translate_mouse(mouse).map(Event::Mouse),
809        ct::Event::Paste(text) => Some(Event::Paste(text)),
810        ct::Event::FocusGained | ct::Event::FocusLost | ct::Event::Resize(..) => None,
811    }
812}
813
814fn modifiers(mods: ct::KeyModifiers) -> Modifiers {
815    Modifiers {
816        ctrl: mods.contains(ct::KeyModifiers::CONTROL),
817        alt: mods.contains(ct::KeyModifiers::ALT),
818        shift: mods.contains(ct::KeyModifiers::SHIFT),
819    }
820}
821
822fn translate_key(key: ct::KeyEvent) -> Option<KeyEvent> {
823    let mut mods = modifiers(key.modifiers);
824    let code = match key.code {
825        ct::KeyCode::Char(' ') => Key::Space,
826        ct::KeyCode::Char(c) if c.is_uppercase() => {
827            mods.shift = true;
828            Key::Char(c.to_lowercase().next().unwrap_or(c))
829        }
830        ct::KeyCode::Char(c) => {
831            if !c.is_alphabetic() {
832                mods.shift = false;
833            }
834            Key::Char(c)
835        }
836        ct::KeyCode::Enter => Key::Enter,
837        ct::KeyCode::Esc => Key::Esc,
838        ct::KeyCode::Tab => Key::Tab,
839        ct::KeyCode::BackTab => {
840            mods.shift = true;
841            Key::Tab
842        }
843        ct::KeyCode::Backspace => Key::Backspace,
844        ct::KeyCode::Delete => Key::Delete,
845        ct::KeyCode::Insert => Key::Insert,
846        ct::KeyCode::Home => Key::Home,
847        ct::KeyCode::End => Key::End,
848        ct::KeyCode::PageUp => Key::PageUp,
849        ct::KeyCode::PageDown => Key::PageDown,
850        ct::KeyCode::Up => Key::Up,
851        ct::KeyCode::Down => Key::Down,
852        ct::KeyCode::Left => Key::Left,
853        ct::KeyCode::Right => Key::Right,
854        ct::KeyCode::F(n) => Key::F(n),
855        ct::KeyCode::Menu => Key::Menu,
856        _ => return None,
857    };
858    let kind = match key.kind {
859        ct::KeyEventKind::Press => KeyKind::Press,
860        ct::KeyEventKind::Repeat => KeyKind::Repeat,
861        ct::KeyEventKind::Release => KeyKind::Release,
862    };
863    let text = match key.code {
864        ct::KeyCode::Char(c) if !mods.ctrl && !mods.alt => Some(c),
865        _ => None,
866    };
867    Some(KeyEvent { chord: KeyChord { key: code, mods }, kind, text })
868}
869
870fn translate_mouse(mouse: ct::MouseEvent) -> Option<MouseEvent> {
871    let button = |b: ct::MouseButton| match b {
872        ct::MouseButton::Left => MouseButton::Left,
873        ct::MouseButton::Right => MouseButton::Right,
874        ct::MouseButton::Middle => MouseButton::Middle,
875    };
876    let kind = match mouse.kind {
877        ct::MouseEventKind::Down(b) => MouseKind::Down(button(b)),
878        ct::MouseEventKind::Up(b) => MouseKind::Up(button(b)),
879        ct::MouseEventKind::Drag(b) => MouseKind::Drag(button(b)),
880        ct::MouseEventKind::Moved => MouseKind::Moved,
881        ct::MouseEventKind::ScrollUp => MouseKind::ScrollUp,
882        ct::MouseEventKind::ScrollDown => MouseKind::ScrollDown,
883        ct::MouseEventKind::ScrollLeft | ct::MouseEventKind::ScrollRight => return None,
884    };
885    Some(MouseEvent { kind, x: i32::from(mouse.column), y: i32::from(mouse.row), mods: modifiers(mouse.modifiers) })
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891
892    /// Keeps every way of drawing pictures it hears.
893    struct Told(Vec<crate::graphics::Graphics>);
894
895    impl App for Told {
896        type Msg = crate::graphics::Graphics;
897        fn update(&mut self, graphics: Self::Msg) -> crate::runtime::Command<Self::Msg> {
898            self.0.push(graphics);
899            crate::runtime::Command::none()
900        }
901        fn view(&self, _ui: &mut crate::widget::View<'_, Self::Msg>) {}
902        fn graphics(&self, graphics: crate::graphics::Graphics) -> Option<Self::Msg> {
903            Some(graphics)
904        }
905    }
906
907    #[test]
908    fn a_late_kitty_answer_turns_pictures_to_kitty_and_the_application_hears_it() {
909        use crate::graphics::Graphics;
910        let mut engine = Engine::new(Told(Vec::new()), Env::builtin(), TaskMode::Inline);
911        let area = ratatui_core::layout::Rect::new(0, 0, 10, 4);
912        let mut buffer = ratatui_core::buffer::Buffer::empty(area);
913        engine.render(&mut buffer, Duration::ZERO);
914        assert_eq!(engine.app.0, [Graphics::HalfBlock], "no answer in time");
915        engine.dirty = false;
916        heard_kitty_late(&mut engine);
917        assert!(engine.dirty, "a frame is due");
918        engine.render(&mut buffer, Duration::from_secs(1));
919        assert_eq!(engine.app.0, [Graphics::HalfBlock, Graphics::Kitty]);
920        assert_eq!(engine.env.graphics(), Graphics::Kitty);
921    }
922
923    #[test]
924    fn a_cell_is_the_window_divided_by_its_columns_and_rows() {
925        assert_eq!(cell_of(80, 24, 800, 480), Some((10, 20)));
926        assert_eq!(cell_of(100, 30, 905, 571), Some((9, 19)), "a margin is left out");
927        assert_eq!(cell_of(80, 24, 0, 0), None, "a terminal that reports no pixels");
928        assert_eq!(cell_of(0, 0, 800, 480), None);
929        assert_eq!(cell_of(80, 24, 40, 480), None, "fewer pixels than columns");
930    }
931
932    /// Keeps the cell sizes its views are told.
933    struct Seen(std::cell::RefCell<Vec<Option<(u16, u16)>>>);
934
935    impl App for Seen {
936        type Msg = ();
937        fn update(&mut self, (): ()) -> crate::runtime::Command<()> {
938            crate::runtime::Command::none()
939        }
940        fn view(&self, ui: &mut crate::widget::View<'_, ()>) {
941            self.0.borrow_mut().push(ui.env().cell_pixels());
942        }
943    }
944
945    #[test]
946    fn a_new_cell_size_is_a_new_frame_and_the_same_one_is_not() {
947        let mut engine = Engine::new(Seen(std::cell::RefCell::default()), Env::builtin(), TaskMode::Inline);
948        took_cell(&mut engine, Some((10, 20)));
949        let area = ratatui_core::layout::Rect::new(0, 0, 10, 4);
950        let mut buffer = ratatui_core::buffer::Buffer::empty(area);
951        engine.render(&mut buffer, Duration::ZERO);
952        engine.dirty = false;
953        took_cell(&mut engine, Some((10, 20)));
954        assert!(!engine.dirty, "nothing changed");
955        took_cell(&mut engine, Some((14, 28)));
956        assert!(engine.dirty, "a font grew under the same columns and rows");
957        engine.render(&mut buffer, Duration::from_secs(1));
958        assert_eq!(*engine.app.0.borrow(), [Some((10, 20)), Some((14, 28))]);
959    }
960
961    #[test]
962    fn panics_on_other_threads_leave_the_terminal_alone() {
963        use std::sync::Arc;
964        use std::sync::atomic::{AtomicUsize, Ordering};
965        let restores = Arc::new(AtomicUsize::new(0));
966        let counter = Arc::clone(&restores);
967        on_panic_in_this_thread(move || {
968            counter.fetch_add(1, Ordering::SeqCst);
969        });
970        // A task's panic is caught and becomes its outcome; the application keeps running.
971        let _ = std::thread::spawn(|| panic!("a background task failed")).join();
972        assert_eq!(restores.load(Ordering::SeqCst), 0, "the terminal stays in application mode");
973        let _ = std::panic::catch_unwind(|| panic!("the runtime failed"));
974        assert_eq!(restores.load(Ordering::SeqCst), 1, "a panic of the runtime thread restores it");
975    }
976
977    /// Output that cannot be written, as when the terminal went away.
978    struct Broken;
979
980    impl Write for Broken {
981        fn write(&mut self, _: &[u8]) -> io::Result<usize> {
982            Err(io::Error::other("the terminal is gone"))
983        }
984
985        fn flush(&mut self) -> io::Result<()> {
986            Err(io::Error::other("the terminal is gone"))
987        }
988    }
989
990    #[test]
991    fn raw_mode_is_left_even_when_the_screen_cannot_be_written() {
992        let mut raw_left = false;
993        let result = give_back(&mut Broken, true, || {
994            raw_left = true;
995            Ok(())
996        });
997        assert!(raw_left, "raw mode is a terminal setting, not output, and is always left");
998        assert_eq!(result.expect_err("the failure is reported").to_string(), "the terminal is gone");
999    }
1000
1001    #[test]
1002    fn leaving_application_mode_writes_every_step_after_one_fails() {
1003        let mut out = Vec::new();
1004        let result = give_back(&mut out, true, || Err(io::Error::other("no raw mode")));
1005        assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
1006        let text = String::from_utf8(out).expect("escape codes");
1007        assert!(text.contains("\x1b[?1049l"), "the alternate screen was left: {text:?}");
1008        assert!(text.contains("\x1b[?25h"), "the cursor is shown again: {text:?}");
1009    }
1010
1011    #[test]
1012    fn taking_the_terminal_back_goes_on_when_raw_mode_fails() {
1013        // Without the alternate screen the application would draw over the shell's own lines.
1014        let mut out = Vec::new();
1015        let result = take_back(&mut out, false, || Err(io::Error::other("no raw mode")));
1016        assert_eq!(result.expect_err("the failure is reported").to_string(), "no raw mode");
1017        let text = String::from_utf8(out).expect("escape codes");
1018        assert!(text.contains("\x1b[?1049h"), "the alternate screen is entered again: {text:?}");
1019    }
1020
1021    #[test]
1022    fn translates_uppercase_and_backtab() {
1023        let key = |code, mods| ct::KeyEvent::new(code, mods);
1024        let a = translate_key(key(ct::KeyCode::Char('A'), ct::KeyModifiers::SHIFT)).expect("key");
1025        assert_eq!(a.chord, "shift+a".parse().expect("chord"));
1026        assert_eq!(a.text, Some('A'));
1027        let question = translate_key(key(ct::KeyCode::Char('?'), ct::KeyModifiers::SHIFT)).expect("key");
1028        assert_eq!(question.chord, "?".parse().expect("chord"));
1029        let back = translate_key(key(ct::KeyCode::BackTab, ct::KeyModifiers::SHIFT)).expect("key");
1030        assert_eq!(back.chord, "shift+tab".parse().expect("chord"));
1031        let ctrl = translate_key(key(ct::KeyCode::Char('q'), ct::KeyModifiers::CONTROL)).expect("key");
1032        assert_eq!(ctrl.chord, "ctrl+q".parse().expect("chord"));
1033        assert_eq!(ctrl.text, None);
1034        let menu = translate_key(key(ct::KeyCode::Menu, ct::KeyModifiers::NONE)).expect("key");
1035        assert_eq!(menu.chord, "menu".parse().expect("chord"));
1036    }
1037}