Skip to main content

qframe/runtime/
app.rs

1//! The application trait.
2
3use super::clipboard::ClipboardEvent;
4use super::command::Command;
5use super::frame_limit::FrameLimit;
6use super::termination::Termination;
7use crate::geometry::Size;
8use crate::graphics::Graphics;
9use crate::storage::Preferences;
10use crate::widget::View;
11
12/// An application built with quvyta-framework: data, a function that draws it and a function that
13/// changes it.
14///
15/// An application implements this trait and runs in a [`Runtime`](super::Runtime), or in a
16/// [`Harness`](super::Harness) for tests.
17///
18/// ```
19/// use qframe::prelude::*;
20///
21/// struct Counter {
22///     value: i32,
23/// }
24///
25/// #[derive(Clone)]
26/// enum Msg {
27///     Increment,
28/// }
29///
30/// impl App for Counter {
31///     type Msg = Msg;
32///
33///     fn update(&mut self, msg: Msg) -> Command<Msg> {
34///         match msg {
35///             Msg::Increment => self.value += 1,
36///         }
37///         Command::none()
38///     }
39///
40///     fn view(&self, ui: &mut View<'_, Msg>) {
41///         ui.column(|ui| {
42///             ui.add(Text::new(format!("Value: {}", self.value)));
43///             ui.add(Button::new("Increment").on_press(Msg::Increment));
44///         });
45///     }
46/// }
47///
48/// let mut app = Harness::new(Counter { value: 0 }, 30, 4);
49/// app.press("tab").press("enter");
50/// assert!(app.screen().contains("Value: 1"));
51/// ```
52///
53/// # Lifecycle
54///
55/// Besides `update` and `view`, six optional hooks follow the application through its life.
56/// Each has a default, so an application implements only the ones it needs:
57///
58/// 1. [`App::resized`] hears the size of the screen: first when the application starts, then
59///    whenever it changes.
60/// 2. [`App::graphics`] hears the way the terminal draws pictures, right after that first size
61///    and whenever it changes, so a picture is decoded at the size it will be shown.
62/// 3. [`App::preferences`] hears the ecosystem's shared preferences of an application started
63///    with [`Runtime::member`](super::Runtime::member): right after the graphics, and whenever
64///    another application changes them while this one runs.
65/// 4. [`App::init`] runs once, right after the first size, graphics and preferences, before the
66///    first frame is built.
67/// 5. [`App::before_quit`] is asked whenever the runtime is about to quit on the user's behalf.
68/// 6. [`App::terminating`] hears that the system is ending the application: a `SIGTERM` or a
69///    `SIGHUP`, when the SSH connection or the terminal went away. It is the one chance to save.
70///
71/// The hooks that only report something ([`App::resized`], [`App::graphics`],
72/// [`App::preferences`], [`App::before_quit`], [`App::terminating`], like
73/// [`App::action`] and [`App::clipboard`]) read the state and answer with a message, which then
74/// goes through `update` like every other; the one that starts work ([`App::init`]) returns a
75/// [`Command`] like `update` does. The [`Harness`](super::Harness) runs every hook exactly
76/// where the terminal runtime does, so a test sees what a user sees.
77///
78/// ```
79/// use qframe::prelude::*;
80///
81/// #[derive(Default)]
82/// struct Editor {
83///     size: Size,
84///     unsaved: bool,
85///     asking: bool,
86/// }
87///
88/// #[derive(Clone)]
89/// enum Msg {
90///     Resized(Size),
91///     AskBeforeQuit,
92///     Quit,
93/// }
94///
95/// impl App for Editor {
96///     type Msg = Msg;
97///
98///     fn init(&mut self) -> Command<Msg> {
99///         // The first key already reaches the list.
100///         Command::focus("files")
101///     }
102///
103///     fn resized(&self, size: Size) -> Option<Msg> {
104///         Some(Msg::Resized(size))
105///     }
106///
107///     fn before_quit(&self) -> Option<Msg> {
108///         self.unsaved.then_some(Msg::AskBeforeQuit)
109///     }
110///
111///     fn update(&mut self, msg: Msg) -> Command<Msg> {
112///         match msg {
113///             Msg::Resized(size) => self.size = size,
114///             Msg::AskBeforeQuit => self.asking = true,
115///             // Decided: this quit does not ask again.
116///             Msg::Quit => return Command::quit(),
117///         }
118///         Command::none()
119///     }
120///
121///     fn view(&self, ui: &mut View<'_, Msg>) {
122///         ui.add(List::new(["notes.md", "todo.md"].map(ListItem::new))).id("files");
123///     }
124/// }
125///
126/// let mut app = Harness::new(Editor { unsaved: true, ..Editor::default() }, 40, 6);
127/// assert!(app.is_focused("files"));
128/// assert_eq!(app.app().size, Size::new(40, 6));
129/// app.resize(30, 4);
130/// assert_eq!(app.app().size, Size::new(30, 4));
131/// app.press("ctrl+q");
132/// assert!(app.app().asking && !app.quit_requested());
133/// app.send(Msg::Quit);
134/// assert!(app.quit_requested());
135/// ```
136pub trait App: 'static {
137    /// Everything that can happen in the application.
138    type Msg: Send + 'static;
139
140    /// Applies a message and returns work for the runtime to do.
141    fn update(&mut self, msg: Self::Msg) -> Command<Self::Msg>;
142
143    /// Describes the screen. Runs after every change; must not do I/O.
144    fn view(&self, ui: &mut View<'_, Self::Msg>);
145
146    /// Turns an `[app]` keymap action into a message, e.g. `"save"` into `Msg::Save`.
147    fn action(&self, _name: &str) -> Option<Self::Msg> {
148        None
149    }
150
151    /// Runs once when the application starts and returns its first work: the focus the first key
152    /// should reach, a tick to start, a dialog to open, a file to read.
153    ///
154    /// It runs at the start of the first frame, after the first [`App::resized`] message and
155    /// before the view of that frame is built, so the first frame already shows what it
156    /// changed. A [`Command::focus`] it returns names a widget that is not on screen yet; focus
157    /// reaches it as soon as that first frame is painted, before the runtime reads any input,
158    /// and the frame is drawn again at once with the widget focused. The first key the user
159    /// presses therefore reaches the focused widget.
160    ///
161    /// The runtime calls it once per run, the [`Harness`](super::Harness) once when it is
162    /// created. The default does nothing.
163    fn init(&mut self) -> Command<Self::Msg> {
164        Command::none()
165    }
166
167    /// Hears the size of the screen, in columns and rows: when the application starts, before
168    /// [`App::init`], and afterwards whenever the terminal is resized. The message it returns
169    /// goes through [`App::update`], which is where work that needs the size starts, such as
170    /// [`Process::pty`](super::Process::pty) with the width and height the output will have.
171    ///
172    /// It is the size [`View::size`] reports: the terminal size of the frame about to be drawn.
173    /// The message is applied before that frame's view is built, so `update` and `view` never
174    /// disagree about it. A resize that ends at the size already reported is not reported
175    /// again. [`Harness::new`](super::Harness::new) reports the size it is given, and
176    /// [`Harness::resize`](super::Harness::resize) the new one.
177    ///
178    /// The default ignores the size.
179    fn resized(&self, _size: Size) -> Option<Self::Msg> {
180        None
181    }
182
183    /// Hears the way this terminal draws pictures, [`Env::graphics`](crate::env::Env::graphics):
184    /// when the application starts, after [`App::resized`] and before [`App::init`], and
185    /// afterwards whenever it changes, such as when the glyph mode is switched to ASCII or back
186    /// while the application runs, or when the terminal's kitty answer arrives late over a slow
187    /// link. The message it returns goes through [`App::update`], which is where a picture is
188    /// decoded at the size the terminal shows: about ten by twenty pixels a cell for
189    /// [`Graphics::Kitty`], one pixel wide and two tall for half blocks, and not at all where
190    /// [`Graphics::can_draw`] is false.
191    ///
192    /// Like the size, the message is applied before the frame whose view first sees the new
193    /// value is built, and a value already reported is not reported again.
194    /// [`Harness::set_graphics`](super::Harness::set_graphics),
195    /// [`Harness::set_depth`](super::Harness::set_depth) and
196    /// [`Harness::set_glyph_mode`](super::Harness::set_glyph_mode) report a change the way the
197    /// runtime does.
198    ///
199    /// The default ignores it.
200    fn graphics(&self, _graphics: Graphics) -> Option<Self::Msg> {
201        None
202    }
203
204    /// Hears the ecosystem's shared preferences, for an application started as a member of an
205    /// ecosystem with [`Runtime::member`](super::Runtime::member): once when it starts, after
206    /// [`App::graphics`] and before [`App::init`], with what it starts with; and afterwards
207    /// whenever the ecosystem's shared file or the application's own file changes what they
208    /// resolve to, such as when another application of the ecosystem switches the theme for all
209    /// of them.
210    ///
211    /// The runtime has already switched the screen by then: language, theme, icons and reduced
212    /// motion as the ecosystem resolves them, and the pillar when the application's own file
213    /// changed it. Nothing has to be applied here. An application with a settings screen
214    /// refreshes it here, so an open screen shows the new values: an
215    /// [`Appearance`](crate::widgets::Appearance) section takes them with
216    /// [`Appearance::refresh`](crate::widgets::Appearance::refresh).
217    ///
218    /// A file written again with what it already said is not reported, so an application that
219    /// saves its own change hears at most what it saved, once, and never loops. An application
220    /// started without [`Runtime::member`](super::Runtime::member) is never told.
221    /// [`Harness::member_in`](super::Harness::member_in) and
222    /// [`Harness::member_in_with_env`](super::Harness::member_in_with_env) start a test the same
223    /// way, and [`Harness::poll_preferences`](super::Harness::poll_preferences) reads the files
224    /// again as the runtime does when they change.
225    ///
226    /// ```
227    /// use qframe::prelude::*;
228    /// use qframe::storage::{Ecosystem, Preferences, Scope, Shared};
229    ///
230    /// #[derive(Default)]
231    /// struct Notes {
232    ///     theme: String,
233    /// }
234    ///
235    /// enum Msg {
236    ///     Preferences(Preferences),
237    /// }
238    ///
239    /// impl App for Notes {
240    ///     type Msg = Msg;
241    ///
242    ///     fn preferences(&self, preferences: &Preferences) -> Option<Msg> {
243    ///         Some(Msg::Preferences(preferences.clone()))
244    ///     }
245    ///
246    ///     fn update(&mut self, msg: Msg) -> Command<Msg> {
247    ///         match msg {
248    ///             Msg::Preferences(preferences) => self.theme = preferences.theme().value.clone(),
249    ///         }
250    ///         Command::none()
251    ///     }
252    ///
253    ///     fn view(&self, ui: &mut View<'_, Msg>) {
254    ///         ui.add(Text::new(self.theme.clone()));
255    ///     }
256    /// }
257    ///
258    /// # let folder = std::env::temp_dir().join(format!("quvyta-app-preferences-doc-{}", std::process::id()));
259    /// # std::fs::remove_dir_all(&folder).ok();
260    /// let ecosystem = Ecosystem::QUVYTA;
261    /// let mut app = Harness::member_in(Notes::default(), ecosystem, &folder, "notes", 30, 3);
262    /// assert_eq!(app.app().theme, "monochrome");
263    /// // Another application switches every follower to amber.
264    /// ecosystem.set_in(&folder, "desk", Shared::Theme, "amber", Scope::Ecosystem).expect("saved");
265    /// app.poll_preferences();
266    /// assert_eq!(app.app().theme, "amber");
267    /// assert_eq!(app.env().theme().id(), "amber");
268    /// # std::fs::remove_dir_all(&folder).ok();
269    /// ```
270    fn preferences(&self, _preferences: &Preferences) -> Option<Self::Msg> {
271        None
272    }
273
274    /// Asked whenever the runtime is about to quit on the user's behalf: the global `quit`
275    /// action of the keymap, however it was reached (its key, the command palette, a widget
276    /// that runs the action). `None` lets the runtime quit. A message keeps the application running and is
277    /// delivered through [`App::update`] instead, e.g. to ask "finish and quit, keep running or
278    /// cancel" first.
279    ///
280    /// Once the application has decided, it quits with [`Command::quit`], which is its own
281    /// decision and is never asked about. While an answer is pending the user may ask to quit
282    /// again, and the hook is asked again; it sees its own state and can, say, keep the
283    /// question it already shows.
284    ///
285    /// The default lets every quit through.
286    fn before_quit(&self) -> Option<Self::Msg> {
287        None
288    }
289
290    /// Hears that the system is ending the application, and why: see [`Termination`] for each
291    /// cause and the signal behind it. `None` quits at once. A message keeps the application
292    /// running and is delivered through [`App::update`] instead, which is where it saves and
293    /// then returns [`Command::quit`].
294    ///
295    /// The run ends in bounded time whatever the answer: after [`Termination::grace`] the runtime
296    /// quits without the application, and a second `SIGTERM` or `SIGINT` quits at once. After a
297    /// [`Termination::Hangup`] the terminal is usually gone, so nothing is drawn any more and a
298    /// dialog would wait for nobody; save without asking. Work of [`Command::perform`] and
299    /// tasks still run and deliver their messages until the run ends.
300    ///
301    /// The runtime tells the application once per cause: a hangup that repeats is not told
302    /// again, a hangup during a pending terminate is. [`Harness::terminate`](super::Harness::terminate)
303    /// simulates each cause in tests.
304    ///
305    /// The default answers a [`Termination::Terminate`] like a quit the user asked for, with
306    /// [`App::before_quit`], and quits at once on a [`Termination::Hangup`]. So an application
307    /// that implements neither hook quits cleanly on every signal, and one that asks before
308    /// quitting asks on a `SIGTERM` too.
309    ///
310    /// ```
311    /// use qframe::prelude::*;
312    /// use qframe::runtime::Termination;
313    ///
314    /// #[derive(Default)]
315    /// struct Timer {
316    ///     running: bool,
317    ///     saved: bool,
318    /// }
319    ///
320    /// #[derive(Clone)]
321    /// enum Msg {
322    ///     SaveAndQuit,
323    /// }
324    ///
325    /// impl App for Timer {
326    ///     type Msg = Msg;
327    ///
328    ///     fn terminating(&self, _cause: Termination) -> Option<Msg> {
329    ///         // Whether a person or the system ends it, a running timer is saved first.
330    ///         self.running.then_some(Msg::SaveAndQuit)
331    ///     }
332    ///
333    ///     fn update(&mut self, msg: Msg) -> Command<Msg> {
334    ///         match msg {
335    ///             Msg::SaveAndQuit => {
336    ///                 self.saved = true;
337    ///                 Command::quit()
338    ///             }
339    ///         }
340    ///     }
341    ///
342    ///     fn view(&self, ui: &mut View<'_, Msg>) {
343    ///         ui.add(Text::new("25:00"));
344    ///     }
345    /// }
346    ///
347    /// let mut app = Harness::new(Timer { running: true, ..Timer::default() }, 20, 3);
348    /// app.terminate(Termination::Hangup);
349    /// assert!(app.app().saved && app.quit_requested());
350    /// ```
351    fn terminating(&self, cause: Termination) -> Option<Self::Msg> {
352        match cause {
353            Termination::Terminate => self.before_quit(),
354            Termination::Hangup => None,
355        }
356    }
357
358    /// How many frames a second the runtime draws at most; see [`FrameLimit`].
359    ///
360    /// Asked before every frame, so an application may answer from its own state, such as a
361    /// setting the user changed. The default draws 60 frames a second locally and 20 over a
362    /// remote connection. The frames the application's own work causes are merged, and so are
363    /// the pointer's motions and the wheel; a frame that answers a key, a paste, a press or a
364    /// release is never held back.
365    ///
366    /// ```
367    /// # use qframe::prelude::*;
368    /// # use qframe::runtime::FrameLimit;
369    /// # struct Desktop;
370    /// # impl App for Desktop {
371    /// #     type Msg = ();
372    /// #     fn update(&mut self, (): ()) -> Command<()> {
373    /// #         Command::none()
374    /// #     }
375    /// // A desktop of terminal windows spends a slow link on ten frames a second.
376    /// fn frame_limit(&self) -> FrameLimit {
377    ///     FrameLimit::per_second(60).remote(10)
378    /// }
379    /// #     fn view(&self, ui: &mut View<'_, ()>) {
380    /// #         ui.add(Text::new("windows"));
381    /// #     }
382    /// # }
383    /// ```
384    fn frame_limit(&self) -> FrameLimit {
385        FrameLimit::default()
386    }
387
388    /// Hears about the clipboard: text a widget or a mouse selection copied, and pasted text
389    /// that no focused widget took. Copies the application asked for with
390    /// [`Command::copy`] are not reported, so answering a copy with a copy cannot loop.
391    fn clipboard(&self, _event: &ClipboardEvent) -> Option<Self::Msg> {
392        None
393    }
394}