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