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 #[must_use]
79 pub fn with_handoff(mut self, handoff: Option<&'a PlaybackHandoff>) -> Self {
80 self.handoff = handoff;
81 self
82 }
83
84 /// This launch, also naming the app's own `options`.
85 #[must_use]
86 pub fn with_options(mut self, options: &'a [LaunchOption]) -> Self {
87 self.options = options;
88 self
89 }
90
91 /// The launch's arguments, in order; at most
92 /// [`ResourceApp::ARGUMENTS`](super::ResourceApp::ARGUMENTS) of them.
93 pub fn arguments(&self) -> &'a [String] {
94 self.arguments
95 }
96
97 /// The first argument, for an app that takes one (a path, a URL).
98 pub fn argument(&self) -> Option<&'a str> {
99 self.arguments.first().map(String::as_str)
100 }
101
102 /// The file a host handed over to open: its name, media type and bytes.
103 /// Only a file of a type the app
104 /// [opens](super::ResourceApp::OPENS) reaches it.
105 pub fn file(&self) -> Option<&'a OpenFile> {
106 self.file
107 }
108
109 /// The content store of the host the app is launched in: a handed
110 /// file's bytes are kept there, and the launch's actions name them by
111 /// hash, so the record never carries a file and every device that holds
112 /// the bytes reads them. `None` where the host lends none.
113 pub fn content(&self) -> Option<&'a tilo_content::ContentStore> {
114 self.content
115 }
116
117 /// Where playback of the media the launch names stood on the device or
118 /// app that handed it over: the position to continue from, whether it
119 /// keeps playing and what it was called there. `None` for an ordinary
120 /// launch, which starts at the beginning.
121 pub fn handoff(&self) -> Option<&'a PlaybackHandoff> {
122 self.handoff
123 }
124
125 /// The value of the app's option `name` (`--purpose`), when the launch
126 /// named it; see [`ResourceApp::OPTIONS`](super::ResourceApp::OPTIONS).
127 pub fn option(&self, name: &str) -> Option<&'a str> {
128 option(self.options, name)
129 }
130
131 /// Whether the launch named the app's switch `name`; see
132 /// [`ResourceApp::FLAGS`](super::ResourceApp::FLAGS).
133 pub fn flag(&self, name: &str) -> bool {
134 flag(self.options, name)
135 }
136
137 /// Whether the launch named nothing: a plain start of the app.
138 pub fn is_empty(&self) -> bool {
139 self.arguments.is_empty() && self.options.is_empty() && self.file.is_none()
140 }
141}
142
143pub(crate) fn option<'a>(options: &'a [LaunchOption], name: &str) -> Option<&'a str> {
144 options
145 .iter()
146 .find(|option| option.name == name)
147 .and_then(|option| option.value.as_deref())
148}
149
150pub(crate) fn flag(options: &[LaunchOption], name: &str) -> bool {
151 options
152 .iter()
153 .any(|option| option.name == name && option.value.is_none())
154}
155
156/// A command the app's binary runs instead of opening a window: a developer
157/// utility (writing fixtures, staging a repository) that shares the app's
158/// code but is not part of using it.
159#[derive(Clone, Copy, Debug)]
160pub struct Command {
161 /// The option that runs the command, taking one value: `--emit-photos`.
162 pub option: &'static str,
163 /// Runs the command with the option's value. The process exits after it,
164 /// reporting the error, if any.
165 pub run: fn(&str) -> Result<(), String>,
166}
167
168/// A helper the app's binary is besides the app: a privileged writer, a
169/// staging step, a packaging probe. Asked first, with the process's whole
170/// command line, before anything of the app starts (no window, no session,
171/// no portal), it answers `None` for a command line that is not its own,
172/// else runs and answers the exit code the process ends with. A refused
173/// command line is an error, which ends the process with code 1.
174#[derive(Clone, Copy)]
175pub struct Helper {
176 /// Runs the helper if `args` (the whole command line, the binary's path
177 /// first) is its own.
178 pub run: fn(&[String]) -> Result<Option<i32>, String>,
179}
180
181/// Opens what a launch naming some arguments opens in the window an app runs
182/// in, in place of the record it shows: what
183/// [`RecordEnv::open_here`](super::RecordEnv::open_here) does, kept by a custom
184/// surface for the input that later asks for it (a file chosen in a picker).
185#[derive(Clone)]
186pub struct OpenHere(std::rc::Rc<dyn Fn(Vec<String>) -> ui_core::application::ReplacementOutcome>);
187
188impl OpenHere {
189 pub(crate) fn new(
190 open: impl Fn(Vec<String>) -> ui_core::application::ReplacementOutcome + 'static,
191 ) -> Self {
192 Self(std::rc::Rc::new(open))
193 }
194
195 /// Ask for what a launch naming `arguments` opens, in the window's
196 /// place. The host opens it after the event that asked; the outcome
197 /// answers whether the window shows it, or why not — the record is
198 /// unavailable, the file unreadable — so the asker can answer the
199 /// operation that asked and tell the person.
200 pub fn open(&self, arguments: &[String]) -> ui_core::application::ReplacementOutcome {
201 (self.0)(arguments.to_vec())
202 }
203}
204
205impl std::fmt::Debug for OpenHere {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 f.write_str("OpenHere")
208 }
209}