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