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, 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, thiserror::Error)]
158pub enum GlobalShortcutChordError {
159    #[error("{0} cannot be a global shortcut key")]
160    InvalidKey(Code),
161    #[error("global shortcut modifiers {0:?} contain unsupported modifier state")]
162    UnsupportedModifiers(Modifiers),
163}
164
165/// Typed failure from application-level global shortcut registration.
166#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
167pub enum GlobalShortcutError {
168    #[error("global shortcuts are unsupported")]
169    Unsupported,
170    #[error("invalid global shortcut: {0}")]
171    InvalidChord(GlobalShortcutChordError),
172    #[error("global shortcut {} is already registered", .0.get())]
173    AlreadyRegistered(GlobalShortcutId),
174    #[error("global shortcut {} is not registered", .0.get())]
175    NotRegistered(GlobalShortcutId),
176    #[error(
177        "global shortcut {:?}+{} conflicts with another registration",
178        .0.modifiers(),
179        .0.key()
180    )]
181    Conflict(GlobalShortcutChord),
182    #[error("global shortcut application service stopped")]
183    ApplicationStopped,
184    #[error("global shortcut backend failed: {0}")]
185    Backend(String),
186}
187
188impl From<GlobalShortcutChordError> for GlobalShortcutError {
189    fn from(value: GlobalShortcutChordError) -> Self {
190        Self::InvalidChord(value)
191    }
192}
193
194/// Native/application events which are not owned by a particular window's
195/// input stream.
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub enum ApplicationActivation {
198    Launch(LaunchActivation),
199    OpenFiles(DocumentActivation),
200    OpenUrls(UrlActivation),
201    Reopen,
202    GlobalShortcutInvoked(GlobalShortcutId),
203}
204
205impl ApplicationActivation {
206    #[must_use]
207    pub fn open_files(paths: impl IntoIterator<Item = impl Into<std::path::PathBuf>>) -> Self {
208        Self::OpenFiles(DocumentActivation::from_paths(paths))
209    }
210
211    #[must_use]
212    pub fn open_urls(urls: impl IntoIterator<Item = Url>) -> Self {
213        Self::OpenUrls(UrlActivation::new(urls))
214    }
215
216    #[must_use]
217    pub fn documents(&self) -> &[DocumentDescriptor] {
218        match self {
219            Self::Launch(launch) => launch.documents().documents(),
220            Self::OpenFiles(documents) => documents.documents(),
221            Self::OpenUrls(_) | Self::Reopen | Self::GlobalShortcutInvoked(_) => &[],
222        }
223    }
224
225    #[must_use]
226    pub fn urls(&self) -> &[Url] {
227        match self {
228            Self::Launch(launch) => launch.urls(),
229            Self::OpenUrls(urls) => urls.urls(),
230            Self::OpenFiles(_) | Self::Reopen | Self::GlobalShortcutInvoked(_) => &[],
231        }
232    }
233}
234
235/// Opt-in policy selecting one primary process for an application identity.
236#[derive(Clone, Debug, PartialEq, Eq, Hash)]
237pub struct SingleInstancePolicy {
238    application_id: Arc<str>,
239}
240
241impl SingleInstancePolicy {
242    pub fn new(application_id: impl Into<String>) -> Result<Self, SingleInstancePolicyError> {
243        let application_id = application_id.into();
244        let valid = !application_id.is_empty()
245            && application_id.len() <= 128
246            && application_id
247                .bytes()
248                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'));
249        if !valid {
250            return Err(SingleInstancePolicyError::InvalidApplicationId(
251                application_id,
252            ));
253        }
254        Ok(Self {
255            application_id: Arc::from(application_id),
256        })
257    }
258
259    #[must_use]
260    pub fn application_id(&self) -> &str {
261        &self.application_id
262    }
263}
264
265#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
266pub enum SingleInstancePolicyError {
267    #[error(
268        "single-instance application id {0:?} must be 1-128 ASCII letters, digits, '.', '_' or '-'"
269    )]
270    InvalidApplicationId(String),
271}