Skip to main content

incular_platform/
application_activation.rs

1//! Application-scoped native activation and global-shortcut vocabulary.
2//!
3//! These values deliberately contain no native window/application handles.
4//! Desktop adapters receive OS events, normalize them here, and hand them to
5//! the runtime's application-level delivery queue.
6
7use crate::{DocumentActivation, DocumentDescriptor};
8use incular_core::{Code, Modifiers};
9use std::{ffi::OsString, fmt, sync::Arc};
10use url::Url;
11
12/// Arguments and typed resources associated with process launch.
13///
14/// `arguments` excludes the executable path and preserves the operating
15/// system's native string representation. `documents` and `urls` are optional
16/// semantic classifications supplied by a platform/packaging adapter; Incular
17/// never guesses that an arbitrary command-line argument is a file or URL.
18#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct LaunchActivation {
20    arguments: Arc<[OsString]>,
21    documents: DocumentActivation,
22    urls: Arc<[Url]>,
23}
24
25impl LaunchActivation {
26    #[must_use]
27    pub fn new(arguments: impl IntoIterator<Item = OsString>) -> Self {
28        Self {
29            arguments: arguments.into_iter().collect::<Vec<_>>().into(),
30            ..Self::default()
31        }
32    }
33
34    /// Captures the current process arguments without the executable name.
35    #[must_use]
36    pub fn current_process() -> Self {
37        Self::new(std::env::args_os().skip(1))
38    }
39
40    #[must_use]
41    pub fn with_documents(mut self, documents: DocumentActivation) -> Self {
42        self.documents = documents;
43        self
44    }
45
46    #[must_use]
47    pub fn with_urls(mut self, urls: impl IntoIterator<Item = Url>) -> Self {
48        self.urls = urls.into_iter().collect::<Vec<_>>().into();
49        self
50    }
51
52    #[must_use]
53    pub fn arguments(&self) -> &[OsString] {
54        &self.arguments
55    }
56
57    #[must_use]
58    pub const fn documents(&self) -> &DocumentActivation {
59        &self.documents
60    }
61
62    #[must_use]
63    pub fn urls(&self) -> &[Url] {
64        &self.urls
65    }
66}
67
68/// Ordered URLs delivered by the operating system or a secondary instance.
69#[derive(Clone, Debug, Default, PartialEq, Eq)]
70pub struct UrlActivation {
71    urls: Arc<[Url]>,
72}
73
74impl UrlActivation {
75    #[must_use]
76    pub fn new(urls: impl IntoIterator<Item = Url>) -> Self {
77        Self {
78            urls: urls.into_iter().collect::<Vec<_>>().into(),
79        }
80    }
81
82    #[must_use]
83    pub fn urls(&self) -> &[Url] {
84        &self.urls
85    }
86
87    #[must_use]
88    pub fn is_empty(&self) -> bool {
89        self.urls.is_empty()
90    }
91}
92
93/// Stable application-owned identity for one native global shortcut.
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
95pub struct GlobalShortcutId(u64);
96
97impl GlobalShortcutId {
98    #[must_use]
99    pub const fn new(value: u64) -> Self {
100        Self(value)
101    }
102
103    #[must_use]
104    pub const fn get(self) -> u64 {
105        self.0
106    }
107}
108
109/// Physical key chord registered with the operating system.
110///
111/// Global shortcuts intentionally use the same standardized physical [`Code`]
112/// and [`Modifiers`] vocabulary as focused keyboard input, while retaining a
113/// separate registration lifecycle.
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115pub struct GlobalShortcutChord {
116    key: Code,
117    modifiers: Modifiers,
118}
119
120impl GlobalShortcutChord {
121    pub fn new(key: Code, modifiers: Modifiers) -> Result<Self, GlobalShortcutChordError> {
122        let supported = Modifiers::ALT | Modifiers::CONTROL | Modifiers::META | Modifiers::SHIFT;
123        if modifiers.bits() & !supported.bits() != 0 {
124            return Err(GlobalShortcutChordError::UnsupportedModifiers(modifiers));
125        }
126        if key == Code::Unidentified || is_modifier_code(key) {
127            return Err(GlobalShortcutChordError::InvalidKey(key));
128        }
129        Ok(Self { key, modifiers })
130    }
131
132    #[must_use]
133    pub const fn key(self) -> Code {
134        self.key
135    }
136
137    #[must_use]
138    pub const fn modifiers(self) -> Modifiers {
139        self.modifiers
140    }
141}
142
143fn is_modifier_code(code: Code) -> bool {
144    matches!(
145        code,
146        Code::AltLeft
147            | Code::AltRight
148            | Code::ControlLeft
149            | Code::ControlRight
150            | Code::MetaLeft
151            | Code::MetaRight
152            | Code::ShiftLeft
153            | Code::ShiftRight
154    )
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub enum GlobalShortcutChordError {
159    InvalidKey(Code),
160    UnsupportedModifiers(Modifiers),
161}
162
163impl fmt::Display for GlobalShortcutChordError {
164    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165        match self {
166            Self::InvalidKey(key) => write!(formatter, "{key} cannot be a global shortcut key"),
167            Self::UnsupportedModifiers(modifiers) => write!(
168                formatter,
169                "global shortcut modifiers {modifiers:?} contain unsupported modifier state"
170            ),
171        }
172    }
173}
174
175impl std::error::Error for GlobalShortcutChordError {}
176
177/// Typed failure from application-level global shortcut registration.
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub enum GlobalShortcutError {
180    Unsupported,
181    InvalidChord(GlobalShortcutChordError),
182    AlreadyRegistered(GlobalShortcutId),
183    NotRegistered(GlobalShortcutId),
184    Conflict(GlobalShortcutChord),
185    ApplicationStopped,
186    Backend(String),
187}
188
189impl fmt::Display for GlobalShortcutError {
190    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
191        match self {
192            Self::Unsupported => formatter.write_str("global shortcuts are unsupported"),
193            Self::InvalidChord(error) => write!(formatter, "invalid global shortcut: {error}"),
194            Self::AlreadyRegistered(id) => {
195                write!(
196                    formatter,
197                    "global shortcut {} is already registered",
198                    id.get()
199                )
200            }
201            Self::NotRegistered(id) => {
202                write!(formatter, "global shortcut {} is not registered", id.get())
203            }
204            Self::Conflict(chord) => write!(
205                formatter,
206                "global shortcut {:?}+{} conflicts with another registration",
207                chord.modifiers(),
208                chord.key()
209            ),
210            Self::ApplicationStopped => {
211                formatter.write_str("global shortcut application service stopped")
212            }
213            Self::Backend(message) => {
214                write!(formatter, "global shortcut backend failed: {message}")
215            }
216        }
217    }
218}
219
220impl std::error::Error for GlobalShortcutError {}
221
222impl From<GlobalShortcutChordError> for GlobalShortcutError {
223    fn from(value: GlobalShortcutChordError) -> Self {
224        Self::InvalidChord(value)
225    }
226}
227
228/// Native/application events which are not owned by a particular window's
229/// input stream.
230#[derive(Clone, Debug, PartialEq, Eq)]
231pub enum ApplicationActivation {
232    Launch(LaunchActivation),
233    OpenFiles(DocumentActivation),
234    OpenUrls(UrlActivation),
235    Reopen,
236    GlobalShortcutInvoked(GlobalShortcutId),
237}
238
239impl ApplicationActivation {
240    #[must_use]
241    pub fn open_files(paths: impl IntoIterator<Item = impl Into<std::path::PathBuf>>) -> Self {
242        Self::OpenFiles(DocumentActivation::from_paths(paths))
243    }
244
245    #[must_use]
246    pub fn open_urls(urls: impl IntoIterator<Item = Url>) -> Self {
247        Self::OpenUrls(UrlActivation::new(urls))
248    }
249
250    #[must_use]
251    pub fn documents(&self) -> &[DocumentDescriptor] {
252        match self {
253            Self::Launch(launch) => launch.documents().documents(),
254            Self::OpenFiles(documents) => documents.documents(),
255            Self::OpenUrls(_) | Self::Reopen | Self::GlobalShortcutInvoked(_) => &[],
256        }
257    }
258
259    #[must_use]
260    pub fn urls(&self) -> &[Url] {
261        match self {
262            Self::Launch(launch) => launch.urls(),
263            Self::OpenUrls(urls) => urls.urls(),
264            Self::OpenFiles(_) | Self::Reopen | Self::GlobalShortcutInvoked(_) => &[],
265        }
266    }
267}
268
269/// Opt-in policy selecting one primary process for an application identity.
270#[derive(Clone, Debug, PartialEq, Eq, Hash)]
271pub struct SingleInstancePolicy {
272    application_id: Arc<str>,
273}
274
275impl SingleInstancePolicy {
276    pub fn new(application_id: impl Into<String>) -> Result<Self, SingleInstancePolicyError> {
277        let application_id = application_id.into();
278        let valid = !application_id.is_empty()
279            && application_id.len() <= 128
280            && application_id
281                .bytes()
282                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'));
283        if !valid {
284            return Err(SingleInstancePolicyError::InvalidApplicationId(
285                application_id,
286            ));
287        }
288        Ok(Self {
289            application_id: Arc::from(application_id),
290        })
291    }
292
293    #[must_use]
294    pub fn application_id(&self) -> &str {
295        &self.application_id
296    }
297}
298
299#[derive(Clone, Debug, PartialEq, Eq)]
300pub enum SingleInstancePolicyError {
301    InvalidApplicationId(String),
302}
303
304impl fmt::Display for SingleInstancePolicyError {
305    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
306        match self {
307            Self::InvalidApplicationId(value) => write!(
308                formatter,
309                "single-instance application id {value:?} must be 1-128 ASCII letters, digits, '.', '_' or '-'"
310            ),
311        }
312    }
313}
314
315impl std::error::Error for SingleInstancePolicyError {}