Skip to main content

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}