1use crate::{DocumentActivation, DocumentDescriptor};
8use incular_core::{Code, Modifiers};
9use std::{ffi::OsString, fmt, sync::Arc};
10use url::Url;
11
12#[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 #[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#[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#[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#[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#[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#[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#[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 {}