tilo_app/framework/launch.rs
1//! What a launch asks an app to open, and the commands its binary runs
2//! instead of opening a window.
3//!
4//! A launch names things beside the app: a file a person opened with it, the
5//! paths or URLs of its command line. The app answers in two steps, each
6//! through a concept it already has. Which record the launch opens is a
7//! [`RecordId`](ui_core::record::RecordId)
8//! ([`ResourceApp::launch_record`](super::ResourceApp::launch_record)): the
9//! file's own record ([`RecordId::identify_file`](ui_core::record::RecordId::identify_file)),
10//! a record derived from it, a minted one. What the launch then does in that
11//! record is the resource's actions
12//! ([`ResourceApp::launch`](super::ResourceApp::launch)), submitted through
13//! the same door as every control, so they are checked, attributed to the
14//! participant, undoable and in history.
15//!
16//! An app's own command-line options
17//! ([`ResourceApp::OPTIONS`](super::ResourceApp::OPTIONS),
18//! [`ResourceApp::FLAGS`](super::ResourceApp::FLAGS)) say how this window
19//! runs rather than what it opens: the layout an on-screen keyboard starts
20//! in, a debug overlay. The launch hands them over beside the arguments, and
21//! views and surfaces read them where they set themselves up
22//! ([`RecordEnv::launch_option`](super::RecordEnv::launch_option)).
23
24use ui_core::application::{LaunchOption, OpenFile, PlaybackHandoff};
25
26/// What a launch handed the app: the arguments of its command line or of an
27/// "open with" (paths, URLs), the app's own options it named, the file a
28/// host lent it, the content store where the app keeps what a launch hands
29/// it, and where playback of the media it names stood on the device or app
30/// that handed it over.
31#[derive(Clone, Copy)]
32pub struct LaunchInput<'a> {
33 arguments: &'a [String],
34 options: &'a [LaunchOption],
35 file: Option<&'a OpenFile>,
36 content: Option<&'a tilo_content::ContentStore>,
37 handoff: Option<&'a PlaybackHandoff>,
38 record: Option<&'a dyn std::any::Any>,
39}
40
41impl<'a> LaunchInput<'a> {
42 /// A launch naming `arguments` and handing over `file`: what a host
43 /// asks [`ResourceApp::launch`](super::ResourceApp::launch) about, and
44 /// what a test of an app's launch builds.
45 pub fn new(arguments: &'a [String], file: Option<&'a OpenFile>) -> Self {
46 Self {
47 arguments,
48 options: &[],
49 file,
50 content: None,
51 handoff: None,
52 record: None,
53 }
54 }
55
56 /// The same launch on a record that is already there, as it is now.
57 #[must_use]
58 pub fn with_record(mut self, state: &'a dyn std::any::Any) -> Self {
59 self.record = Some(state);
60 self
61 }
62
63 /// The state of the record the launch opened, as it is when the launch
64 /// is taken: `S` is the app resource's state. `None` before the record
65 /// is open (a [`genesis`](super::ResourceApp::genesis)) or for another
66 /// type.
67 pub fn record<S: 'static>(&self) -> Option<&'a S> {
68 self.record?.downcast_ref::<S>()
69 }
70
71 /// The same launch on a host that lends the app `content`.
72 #[must_use]
73 pub fn with_content(mut self, content: Option<&'a tilo_content::ContentStore>) -> Self {
74 self.content = content;
75 self
76 }
77
78 /// The same launch, continuing `handoff`'s playback.
79 #[must_use]
80 pub fn with_handoff(mut self, handoff: Option<&'a PlaybackHandoff>) -> Self {
81 self.handoff = handoff;
82 self
83 }
84
85 /// This launch, also naming the app's own `options`.
86 #[must_use]
87 pub fn with_options(mut self, options: &'a [LaunchOption]) -> Self {
88 self.options = options;
89 self
90 }
91
92 /// The launch's arguments, in order; at most
93 /// [`ResourceApp::ARGUMENTS`](super::ResourceApp::ARGUMENTS) of them.
94 pub fn arguments(&self) -> &'a [String] {
95 self.arguments
96 }
97
98 /// The first argument, for an app that takes one (a path, a URL).
99 pub fn argument(&self) -> Option<&'a str> {
100 self.arguments.first().map(String::as_str)
101 }
102
103 /// The file a host handed over to open: its name, media type and bytes.
104 /// Only a file of a type the app
105 /// [opens](super::ResourceApp::OPENS) reaches it.
106 pub fn file(&self) -> Option<&'a OpenFile> {
107 self.file
108 }
109
110 /// The content store of the host the app is launched in: a handed
111 /// file's bytes are kept there, and the launch's actions name them by
112 /// hash, so the record never carries a file and every device that holds
113 /// the bytes reads them. `None` where the host lends none.
114 pub fn content(&self) -> Option<&'a tilo_content::ContentStore> {
115 self.content
116 }
117
118 /// Where playback of the media the launch names stood on the device or
119 /// app that handed it over: the position to continue from, whether it
120 /// keeps playing and what it was called there. `None` for an ordinary
121 /// launch, which starts at the beginning.
122 pub fn handoff(&self) -> Option<&'a PlaybackHandoff> {
123 self.handoff
124 }
125
126 /// The value of the app's option `name` (`--purpose`), when the launch
127 /// named it; see [`ResourceApp::OPTIONS`](super::ResourceApp::OPTIONS).
128 pub fn option(&self, name: &str) -> Option<&'a str> {
129 option(self.options, name)
130 }
131
132 /// Whether the launch named the app's switch `name`; see
133 /// [`ResourceApp::FLAGS`](super::ResourceApp::FLAGS).
134 pub fn flag(&self, name: &str) -> bool {
135 flag(self.options, name)
136 }
137
138 /// Whether the launch named nothing: a plain start of the app.
139 pub fn is_empty(&self) -> bool {
140 self.arguments.is_empty() && self.options.is_empty() && self.file.is_none()
141 }
142}
143
144pub(crate) fn option<'a>(options: &'a [LaunchOption], name: &str) -> Option<&'a str> {
145 options
146 .iter()
147 .find(|option| option.name == name)
148 .and_then(|option| option.value.as_deref())
149}
150
151pub(crate) fn flag(options: &[LaunchOption], name: &str) -> bool {
152 options
153 .iter()
154 .any(|option| option.name == name && option.value.is_none())
155}
156
157/// A command the app's binary runs instead of opening a window: a developer
158/// utility (writing fixtures, staging a repository) that shares the app's
159/// code but is not part of using it.
160#[derive(Clone, Copy, Debug)]
161pub struct Command {
162 /// The option that runs the command, taking one value: `--emit-photos`.
163 pub option: &'static str,
164 /// Runs the command with the option's value. The process exits after it,
165 /// reporting the error, if any.
166 pub run: fn(&str) -> Result<(), String>,
167}
168
169/// A helper the app's binary is besides the app: a privileged writer, a
170/// staging step, a packaging probe. Asked first, with the process's whole
171/// command line, before anything of the app starts (no window, no session,
172/// no portal), it answers `None` for a command line that is not its own,
173/// else runs and answers the exit code the process ends with. A refused
174/// command line is an error, which ends the process with code 1.
175#[derive(Clone, Copy)]
176pub struct Helper {
177 /// Runs the helper if `args` (the whole command line, the binary's path
178 /// first) is its own.
179 pub run: fn(&[String]) -> Result<Option<i32>, String>,
180}
181
182/// Opens what a launch naming some arguments opens in the window an app runs
183/// in, in place of the record it shows: what
184/// [`RecordEnv::open_here`](super::RecordEnv::open_here) does, kept by a custom
185/// surface for the input that later asks for it (a file chosen in a picker).
186#[derive(Clone)]
187pub struct OpenHere(std::rc::Rc<dyn Fn(Vec<String>) -> ui_core::application::ReplacementOutcome>);
188
189impl OpenHere {
190 pub(crate) fn new(
191 open: impl Fn(Vec<String>) -> ui_core::application::ReplacementOutcome + 'static,
192 ) -> Self {
193 Self(std::rc::Rc::new(open))
194 }
195
196 /// Ask for what a launch naming `arguments` opens, in the window's
197 /// place. The host opens it after the event that asked; the outcome
198 /// answers whether the window shows it, or why not — the record is
199 /// unavailable, the file unreadable — so the asker can answer the
200 /// operation that asked and tell the person.
201 pub fn open(&self, arguments: &[String]) -> ui_core::application::ReplacementOutcome {
202 (self.0)(arguments.to_vec())
203 }
204}
205
206impl std::fmt::Debug for OpenHere {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 f.write_str("OpenHere")
209 }
210}