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}