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