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    /// 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}