Skip to main content

qframe/runtime/
command.rs

1//! Work an application asks the runtime to do after an update.
2
3use std::sync::Arc;
4
5use super::confirm::Confirm;
6use super::detached::DetachedHandoff;
7use super::handoff::Handoff;
8use super::open::Open;
9use super::task::{Task, TaskId};
10use crate::icons::IconMode;
11use crate::widgets::{Corner, Toast};
12
13pub(crate) enum Action<Msg> {
14    Quit,
15    Focus(String),
16    SetTheme(String),
17    SetLocale(String),
18    SetRegion(Option<String>),
19    SetIconMode(IconMode),
20    SetReducedMotion(bool),
21    SetPillar(crate::icons::PillarStyle),
22    SetSlide(bool),
23    Copy(String),
24    Confirm(Confirm<Msg>),
25    ReadClipboard(Box<dyn FnOnce(Option<String>) -> Msg>),
26    Perform(Box<dyn FnOnce() -> Msg + Send>),
27    Toast(Toast<Msg>),
28    DismissToast(String),
29    ToastCorner(Corner),
30    Task(Task<Msg>),
31    CancelTask(TaskId),
32    Handoff(Handoff<Msg>),
33    HandoffDetached(DetachedHandoff<Msg>),
34    Open(Open<Msg>),
35    #[cfg(feature = "updates")]
36    CheckForUpdate(super::update_check::UpdateCheck<Msg>),
37}
38
39/// A message conversion shared by every action of a mapped command; the work of tasks and
40/// performs calls it on their own threads.
41pub(crate) type MapFn<A, B> = Arc<dyn Fn(A) -> B + Send + Sync>;
42
43impl<A: Send + 'static> Action<A> {
44    /// The same action delivering `map(message)` wherever it would deliver `message`.
45    fn map<B: Send + 'static>(self, map: &MapFn<A, B>) -> Action<B> {
46        match self {
47            Self::Quit => Action::Quit,
48            Self::Focus(name) => Action::Focus(name),
49            Self::SetTheme(id) => Action::SetTheme(id),
50            Self::SetLocale(code) => Action::SetLocale(code),
51            Self::SetRegion(region) => Action::SetRegion(region),
52            Self::SetIconMode(mode) => Action::SetIconMode(mode),
53            Self::SetReducedMotion(reduced) => Action::SetReducedMotion(reduced),
54            Self::SetPillar(style) => Action::SetPillar(style),
55            Self::SetSlide(slide) => Action::SetSlide(slide),
56            Self::Copy(text) => Action::Copy(text),
57            Self::Confirm(confirm) => Action::Confirm(confirm.map(|message| map(message))),
58            Self::ReadClipboard(message) => {
59                let map = Arc::clone(map);
60                Action::ReadClipboard(Box::new(move |text| map(message(text))))
61            }
62            Self::Perform(work) => {
63                let map = Arc::clone(map);
64                Action::Perform(Box::new(move || map(work())))
65            }
66            Self::Toast(toast) => Action::Toast(toast.map(Arc::clone(map))),
67            Self::DismissToast(key) => Action::DismissToast(key),
68            Self::ToastCorner(corner) => Action::ToastCorner(corner),
69            Self::Task(task) => Action::Task(task.map(Arc::clone(map))),
70            Self::CancelTask(id) => Action::CancelTask(id),
71            Self::Handoff(handoff) => {
72                let map = Arc::clone(map);
73                Action::Handoff(handoff.map(move |message| map(message)))
74            }
75            Self::HandoffDetached(handoff) => Action::HandoffDetached(handoff.map(Arc::clone(map))),
76            Self::Open(open) => {
77                let map = Arc::clone(map);
78                Action::Open(open.map(move |message| map(message)))
79            }
80            #[cfg(feature = "updates")]
81            Self::CheckForUpdate(check) => {
82                let map = Arc::clone(map);
83                Action::CheckForUpdate(check.map(move |message| map(message)))
84            }
85        }
86    }
87}
88
89/// Work for the runtime, returned from [`App::update`](crate::runtime::App::update).
90pub struct Command<Msg> {
91    pub(crate) actions: Vec<Action<Msg>>,
92}
93
94impl<Msg: Send + 'static> Command<Msg> {
95    /// Nothing to do.
96    #[must_use]
97    pub fn none() -> Self {
98        Self { actions: Vec::new() }
99    }
100
101    /// Several commands, run in order.
102    #[must_use]
103    pub fn batch(commands: impl IntoIterator<Item = Self>) -> Self {
104        Self { actions: commands.into_iter().flat_map(|command| command.actions).collect() }
105    }
106
107    /// Leaves the application after this update.
108    #[must_use]
109    pub fn quit() -> Self {
110        Self::single(Action::Quit)
111    }
112
113    /// Moves keyboard focus to the widget named `name` with [`NodeMut::id`](crate::widget::NodeMut::id).
114    /// When no such widget is on screen yet, focus moves to it after the next frame if it
115    /// appears there, so an update can show a widget and focus it at once.
116    #[must_use]
117    pub fn focus(name: impl Into<String>) -> Self {
118        Self::single(Action::Focus(name.into()))
119    }
120
121    /// Switches to theme `id`. An unusable theme falls back to the default and is reported in
122    /// the environment's diagnostics.
123    #[must_use]
124    pub fn set_theme(id: impl Into<String>) -> Self {
125        Self::single(Action::SetTheme(id.into()))
126    }
127
128    /// Switches the language to the locale that serves `code`: a locale code such as `tr`, or a
129    /// language tag such as `en-GB`, which also sets the region; see
130    /// [`I18n::select`](crate::i18n::I18n::select). An unknown language changes nothing and is
131    /// reported in the environment's diagnostics.
132    #[must_use]
133    pub fn set_locale(code: impl Into<String>) -> Self {
134        Self::single(Action::SetLocale(code.into()))
135    }
136
137    /// Sets the region whose conventions apply, such as `GB`, or with `None` leaves them to the
138    /// language again; see [`I18n::set_region`](crate::i18n::I18n::set_region). A code that is not
139    /// a region changes nothing and is reported in the environment's diagnostics.
140    #[must_use]
141    pub fn set_region(region: Option<&str>) -> Self {
142        Self::single(Action::SetRegion(region.map(str::to_owned)))
143    }
144
145    /// Switches between Nerd Font, Unicode, ASCII or detected glyphs.
146    #[must_use]
147    pub fn set_icon_mode(mode: IconMode) -> Self {
148        Self::single(Action::SetIconMode(mode))
149    }
150
151    /// Turns reduced motion on or off: layers appear at once and nothing breathes or spins.
152    /// Has no effect while the `QUVYTA_REDUCED_MOTION` environment variable decides.
153    #[must_use]
154    pub fn set_reduced_motion(reduced: bool) -> Self {
155        Self::single(Action::SetReducedMotion(reduced))
156    }
157
158    /// Draws every pillar in `style` over the theme's choice.
159    #[must_use]
160    pub fn set_pillar(style: crate::icons::PillarStyle) -> Self {
161        Self::single(Action::SetPillar(style))
162    }
163
164    /// Turns the one-cell slide of hovered and selected entries in list structures (lists, menus,
165    /// trees, tables, tab rails, setting rows, dropdown options and tabs) on or off over the
166    /// theme's `motion.slide`. Buttons and other controls never slide.
167    #[must_use]
168    pub fn set_slide(slide: bool) -> Self {
169        Self::single(Action::SetSlide(slide))
170    }
171
172    /// Copies `text` to the system clipboard (OSC 52, which also works over SSH) and to the
173    /// application's in-process clipboard, which keeps pasting inside the application working on
174    /// terminals without OSC 52.
175    #[must_use]
176    pub fn copy(text: impl Into<String>) -> Self {
177        Self::single(Action::Copy(text.into()))
178    }
179
180    /// Reads the clipboard and delivers its text, or `None` when there is none. Like the `paste`
181    /// key and Paste menu entries it tries, in order: the system clipboard through its tool
182    /// (`wl-paste`, `xclip` or `xsel`, `pbpaste`; run without a shell, briefly, off the drawing
183    /// thread), the terminal's clipboard through an OSC 52 query (many terminals do not answer,
184    /// so the wait is short), and the text copied last inside this application (by a widget, a
185    /// selection or [`Command::copy`]). The message arrives in a later update once the text is
186    /// known; drawing never waits for it. Text pasted with the terminal's own paste arrives as
187    /// [`Event::Paste`](crate::event::Event::Paste) instead.
188    #[must_use]
189    pub fn read_clipboard(message: impl FnOnce(Option<String>) -> Msg + 'static) -> Self {
190        Self::single(Action::ReadClipboard(Box::new(message)))
191    }
192
193    /// Runs `work` on a background thread and delivers its message when done. Drawing never
194    /// waits for it.
195    #[must_use]
196    pub fn perform(work: impl FnOnce() -> Msg + Send + 'static) -> Self {
197        Self::single(Action::Perform(Box::new(work)))
198    }
199
200    /// Asks the user a question in a dialog the runtime shows over the application, and
201    /// delivers the message of their answer: the confirm message, or the cancel message (if
202    /// any) for Cancel, Esc and the close mark. Cancel, the safe answer, has focus when the dialog opens.
203    /// Several requests stack; the newest is answered first.
204    ///
205    /// ```
206    /// use qframe::prelude::*;
207    ///
208    /// enum Msg {
209    ///     AskRemove,
210    ///     Remove,
211    /// }
212    ///
213    /// fn update(msg: Msg) -> Command<Msg> {
214    ///     match msg {
215    ///         Msg::AskRemove => Command::confirm(
216    ///             Confirm::new("Remove container?", Msg::Remove).message("Its volumes are deleted too.").danger(),
217    ///         ),
218    ///         Msg::Remove => Command::none(),
219    ///     }
220    /// }
221    /// ```
222    #[must_use]
223    pub fn confirm(confirm: Confirm<Msg>) -> Self {
224        Self::single(Action::Confirm(confirm))
225    }
226
227    /// Shows `toast` in the toast corner, above everything else. It slides in, stays for its
228    /// duration (paused while the pointer is on it) and slides out; a click on its close mark dismisses it.
229    ///
230    /// It never covers an open dialog or other modal layer: it keeps to the rows between its
231    /// corner and the dialog, and when there is no room there it waits, its time stopped,
232    /// until there is, such as when the dialog closes.
233    #[must_use]
234    pub fn toast(toast: Toast<Msg>) -> Self {
235        Self::single(Action::Toast(toast))
236    }
237
238    /// Removes the toast shown with [`Toast::key`] `key`.
239    #[must_use]
240    pub fn dismiss_toast(key: impl Into<String>) -> Self {
241        Self::single(Action::DismissToast(key.into()))
242    }
243
244    /// Stacks toasts in `corner` from now on; bottom right by default.
245    #[must_use]
246    pub fn toast_corner(corner: Corner) -> Self {
247        Self::single(Action::ToastCorner(corner))
248    }
249
250    /// Starts `task` on a background thread. Its `Started` event is applied before this update
251    /// returns; progress, messages and the outcome arrive as the work goes on.
252    #[must_use]
253    pub fn task(task: Task<Msg>) -> Self {
254        Self::single(Action::Task(task))
255    }
256
257    /// Asks task `id` to stop: its sleeps wake at once, [`TaskCx::is_cancelled`](crate::runtime::TaskCx::is_cancelled)
258    /// turns true and it ends as [`TaskOutcome::Cancelled`](crate::runtime::TaskOutcome::Cancelled).
259    /// Asking a finished task does nothing.
260    #[must_use]
261    pub fn cancel_task(id: TaskId) -> Self {
262        Self::single(Action::CancelTask(id))
263    }
264
265    /// Hands the terminal to another program and waits for it: the application leaves raw mode
266    /// and the alternate screen, the program runs attached to the real terminal, and afterwards
267    /// the screen is taken back and drawn again in full. Use it for programs that talk to the
268    /// user themselves, such as `sudo` asking for a password, an editor or a pager. The message
269    /// of [`Handoff::new`] arrives once the application has the screen back. Several handoffs run
270    /// one after another.
271    ///
272    /// ```
273    /// use qframe::prelude::*;
274    /// use qframe::runtime::{Handoff, HandoffOutcome};
275    ///
276    /// enum Msg {
277    ///     Edit,
278    ///     Edited(HandoffOutcome),
279    /// }
280    ///
281    /// fn update(msg: Msg) -> Command<Msg> {
282    ///     match msg {
283    ///         Msg::Edit => Command::handoff(Handoff::new("vi", Msg::Edited).arg("notes.md")),
284    ///         Msg::Edited(_) => Command::none(),
285    ///     }
286    /// }
287    /// ```
288    #[must_use]
289    pub fn handoff(handoff: Handoff<Msg>) -> Self {
290        Self::single(Action::Handoff(handoff))
291    }
292
293    /// Hands the terminal to a program until it writes its first line, then takes the screen
294    /// back and leaves the program running in the background, its standard input and output
295    /// piped to the application. Use it for a program that asks the user something on the
296    /// terminal and then serves the application, such as a privileged helper started through
297    /// `pkexec`. See [`DetachedHandoff`] for the whole course; it queues with
298    /// [`Command::handoff`], one after another.
299    ///
300    /// ```
301    /// use qframe::prelude::*;
302    /// use qframe::runtime::{ChildLine, DetachedHandoff, DetachedOutcome};
303    ///
304    /// enum Msg {
305    ///     Start,
306    ///     Started(DetachedOutcome),
307    ///     Said(ChildLine),
308    /// }
309    ///
310    /// fn update(msg: Msg) -> Command<Msg> {
311    ///     match msg {
312    ///         Msg::Start => Command::handoff_detached(
313    ///             DetachedHandoff::new("sh", Msg::Started).args(["-c", "echo ready; cat"]).on_line(Msg::Said),
314    ///         ),
315    ///         Msg::Started(_) | Msg::Said(_) => Command::none(),
316    ///     }
317    /// }
318    /// ```
319    #[must_use]
320    pub fn handoff_detached(handoff: DetachedHandoff<Msg>) -> Self {
321        Self::single(Action::HandoffDetached(handoff))
322    }
323
324    /// Opens an address, a file or a folder on the person's own desktop, without leaving the
325    /// screen: no step aside, no blink, nothing drawn again.
326    ///
327    /// This is the short way of saying `Command::open_with(Open::new(target))`, for an
328    /// application that has nothing to say about the opening. [`Command::open_with`] takes the
329    /// same opening with a message, a program of its own, arguments, a directory or environment.
330    ///
331    /// ```
332    /// use qframe::prelude::*;
333    ///
334    /// enum Msg {
335    ///     ReadTheGuide,
336    /// }
337    ///
338    /// fn update(msg: Msg) -> Command<Msg> {
339    ///     match msg {
340    ///         Msg::ReadTheGuide => Command::open("https://quvyta.com/guide"),
341    ///     }
342    /// }
343    /// ```
344    #[must_use]
345    pub fn open(target: impl Into<std::ffi::OsString>) -> Self {
346        Self::single(Action::Open(Open::new(target)))
347    }
348
349    /// Carries out `open`: a program started quietly beside the application, with the screen left
350    /// exactly as it is. See [`Open`] for the whole of it.
351    ///
352    /// ```
353    /// use qframe::prelude::*;
354    /// use qframe::runtime::{Open, OpenOutcome};
355    ///
356    /// enum Msg {
357    ///     Opened(OpenOutcome),
358    /// }
359    ///
360    /// fn update(_msg: Msg) -> Command<Msg> {
361    ///     Command::open_with(Open::new("/home/me/notes.pdf").answer(Msg::Opened))
362    /// }
363    /// ```
364    #[must_use]
365    pub fn open_with(open: Open<Msg>) -> Self {
366        Self::single(Action::Open(open))
367    }
368
369    /// Asks the package registry, on a thread of its own, whether a newer version of the
370    /// application is out, and sends the check's message only when one is. At most once a day,
371    /// never while the family's update notice is off, and silent without a network; see
372    /// [`UpdateCheck`](super::UpdateCheck). Needs the `updates` feature.
373    #[cfg(feature = "updates")]
374    #[must_use]
375    pub fn check_for_update(check: super::update_check::UpdateCheck<Msg>) -> Self {
376        Self::single(Action::CheckForUpdate(check))
377    }
378
379    /// The same work delivering `map(message)` wherever it would deliver `message`, so a screen
380    /// with messages of its own can return its commands from the application's `update`:
381    ///
382    /// ```
383    /// use qframe::prelude::*;
384    ///
385    /// mod search {
386    ///     use qframe::prelude::*;
387    ///
388    ///     pub enum Msg {
389    ///         Run,
390    ///         Found(usize),
391    ///     }
392    ///
393    ///     pub fn update(msg: Msg) -> Command<Msg> {
394    ///         match msg {
395    ///             Msg::Run => Command::perform(|| Msg::Found(3)),
396    ///             Msg::Found(_) => Command::none(),
397    ///         }
398    ///     }
399    /// }
400    ///
401    /// enum Msg {
402    ///     Search(search::Msg),
403    /// }
404    ///
405    /// fn update(msg: Msg) -> Command<Msg> {
406    ///     match msg {
407    ///         Msg::Search(msg) => search::update(msg).map(Msg::Search),
408    ///     }
409    /// }
410    /// ```
411    ///
412    /// Every kind of work is carried over: a message the work of [`Command::perform`] or a
413    /// [`Task`] produces later on its own thread (its result, what it sends while it runs, its
414    /// events), the answers of [`Command::confirm`], the action and presses of a toast, the
415    /// clipboard text of [`Command::read_clipboard`], the message after a [`Command::handoff`],
416    /// and the messages of a [`Command::handoff_detached`] and of the child it leaves running.
417    /// Work without messages (focus, theme, copy, cancelling a task) is unchanged.
418    ///
419    /// `map` runs on the threads of that background work, and one command can hold several of
420    /// them, so it is shared rather than copied: it must be `Send` and `Sync`, and it is never
421    /// required to be `Clone`. An enum variant such as `Msg::Search` or a closure over
422    /// `Send + Sync` values qualifies.
423    #[must_use]
424    pub fn map<B: Send + 'static>(self, map: impl Fn(Msg) -> B + Send + Sync + 'static) -> Command<B> {
425        let map: MapFn<Msg, B> = Arc::new(map);
426        Command { actions: self.actions.into_iter().map(|action| action.map(&map)).collect() }
427    }
428
429    fn single(action: Action<Msg>) -> Self {
430        Self { actions: vec![action] }
431    }
432}