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