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