evdev_shortcut/
lib.rs

1//! Global keyboard shortcuts using evdev.
2//!
3//! By connecting to the input devices directly with evdev the shortcuts can work regardless of the environment,
4//! they will work under X11, wayland and in the terminal.
5//!
6//! This does come at the cost of having to run the program with elevated permissions.
7//! See [shortcutd](https://docs.rs/shortcutd/latest/shortcutd/) for a solution to running the elevated input handling in a separate process.
8//!
9//! Example:
10//!
11//! ```rust,no_run
12//! # use std::path::PathBuf;
13//! # use glob::GlobError;
14//! # #[cfg(feature = "listener")]
15//! # use evdev_shortcut::{ShortcutListener};
16//! # use evdev_shortcut::{Shortcut, Modifier, Key};
17//! # use tokio::pin;
18//! # use tokio_stream::StreamExt;
19//! # #[cfg(feature = "listener")]
20//! # #[tokio::main]
21//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! let listener = ShortcutListener::new();
23//! listener.add(Shortcut::new(&[Modifier::Meta], Key::KeyN));
24//!
25//! let devices =
26//!     glob::glob("/dev/input/by-id/*-kbd")?.collect::<Result<Vec<PathBuf>, GlobError>>()?;
27//!
28//! let stream = listener.listen(&devices)?;
29//! pin!(stream);
30//!
31//! while let Some(event) = stream.next().await {
32//!     println!("{} {}", event.shortcut, event.state);
33//! }
34//! # Ok(())
35//! # }
36//! # #[cfg(not(feature = "listener"))]
37//! # fn main() {
38//! # // placeholder so we can build the doc example without default features
39//! # }
40//! ```
41
42pub use keycodes::Key;
43use parse_display::{Display, FromStr, ParseError};
44use std::collections::HashSet;
45use std::fmt::{self, Debug, Display, Formatter};
46use std::path::PathBuf;
47use std::str::FromStr;
48
49mod keycodes;
50
51#[cfg(feature = "listener")]
52mod listener;
53
54#[cfg(feature = "listener")]
55pub use listener::ShortcutListener;
56
57/// Error emitted when an input device can't be opened
58#[derive(Debug, Clone)]
59pub struct DeviceOpenError {
60    pub device: PathBuf,
61}
62
63impl Display for DeviceOpenError {
64    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
65        write!(f, "Failed to open device {:?}", self.device)
66    }
67}
68
69impl std::error::Error for DeviceOpenError {}
70
71/// Modifier key for shortcuts
72#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Display, FromStr)]
73#[repr(u8)]
74pub enum Modifier {
75    Alt,
76    LeftAlt,
77    RightAlt,
78    Ctrl,
79    LeftCtrl,
80    RightCtrl,
81    Shift,
82    LeftShift,
83    RightShift,
84    Meta,
85    LeftMeta,
86    RightMeta,
87}
88
89const ALL_MODIFIERS: &[Modifier] = &[
90    Modifier::Alt,
91    Modifier::LeftAlt,
92    Modifier::RightAlt,
93    Modifier::Ctrl,
94    Modifier::LeftCtrl,
95    Modifier::RightCtrl,
96    Modifier::Shift,
97    Modifier::LeftShift,
98    Modifier::RightShift,
99    Modifier::Meta,
100    Modifier::LeftMeta,
101    Modifier::RightMeta,
102];
103
104const COMBINED_MODIFIERS: &[Modifier] = &[
105    Modifier::Alt,
106    Modifier::Ctrl,
107    Modifier::Shift,
108    Modifier::Meta,
109];
110
111impl Modifier {
112    pub fn mask(&self) -> u8 {
113        match self {
114            Modifier::Alt => 0b00000011,
115            Modifier::LeftAlt => 0b00000001,
116            Modifier::RightAlt => 0b00000010,
117            Modifier::Ctrl => 0b00001100,
118            Modifier::LeftCtrl => 0b00000100,
119            Modifier::RightCtrl => 0b00001000,
120            Modifier::Meta => 0b00110000,
121            Modifier::LeftMeta => 0b00010000,
122            Modifier::RightMeta => 0b00100000,
123            Modifier::Shift => 0b11000000,
124            Modifier::LeftShift => 0b01000000,
125            Modifier::RightShift => 0b10000000,
126        }
127    }
128
129    pub fn mask_from_key(key: Key) -> u8 {
130        match key {
131            Key::KeyLeftAlt => 0b00000001,
132            Key::KeyRightAlt => 0b00000010,
133            Key::KeyLeftCtrl => 0b00000100,
134            Key::KeyRightCtrl => 0b00001000,
135            Key::KeyLeftMeta => 0b00010000,
136            Key::KeyRightMeta => 0b00100000,
137            Key::KeyLeftShift => 0b01000000,
138            Key::KeyRightShift => 0b10000000,
139            _ => 0,
140        }
141    }
142}
143
144/// Set of modifier keys for shortcuts
145#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy, Default)]
146pub struct ModifierList(u8);
147
148impl ModifierList {
149    pub fn new(modifiers: &[Modifier]) -> Self {
150        ModifierList(
151            modifiers
152                .iter()
153                .fold(0, |mask, modifier| mask | modifier.mask()),
154        )
155    }
156
157    pub fn mask(&self) -> u8 {
158        self.0
159    }
160
161    pub fn modifiers(&self) -> impl Iterator<Item = Modifier> {
162        let mask = self.mask();
163        ALL_MODIFIERS.iter().copied().filter(move |modifier| {
164            for combined in COMBINED_MODIFIERS {
165                // if <Ctrl> is enabled, don't emit <LeftCtrl> and <RightCtrl>
166                if combined != modifier
167                    && combined.mask() & modifier.mask() == modifier.mask()
168                    && combined.mask() & mask == combined.mask()
169                {
170                    return false;
171                }
172            }
173            modifier.mask() & mask == modifier.mask()
174        })
175    }
176
177    pub fn len(&self) -> u32 {
178        self.modifiers().count() as u32
179    }
180
181    pub fn is_empty(&self) -> bool {
182        self.mask() == 0
183    }
184}
185
186impl Display for ModifierList {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        for modifier in self.modifiers() {
189            write!(f, "<{}>", modifier)?;
190        }
191        Ok(())
192    }
193}
194
195impl FromStr for ModifierList {
196    type Err = ParseError;
197
198    fn from_str(s: &str) -> Result<Self, Self::Err> {
199        let modifiers = s
200            .split('>')
201            .filter(|part| !part.is_empty())
202            .map(|part| {
203                if !part.starts_with('<') {
204                    Err(ParseError::with_message("Invalid modifier"))
205                } else {
206                    Ok(part[1..].parse::<Modifier>()?)
207                }
208            })
209            .collect::<Result<Vec<Modifier>, ParseError>>()?;
210        Ok(ModifierList::new(&modifiers))
211    }
212}
213
214/// A keyboard shortcut consisting of zero or more modifier keys and a non-modifier key
215///
216/// Examples:
217///
218/// Create from keys:
219///
220/// ```rust
221/// # use evdev_shortcut::{Shortcut, Modifier, Key};
222/// # fn main() {
223/// let shortcut = Shortcut::new(&[Modifier::Meta], Key::KeyN);
224/// # }
225/// ```
226///
227/// Parse from string:
228///
229/// ```rust
230/// # use evdev_shortcut::{Shortcut, Modifier, Key};
231/// # use std::str::FromStr;
232/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
233/// let shortcut: Shortcut = "<Meta>-KeyN".parse()?;
234/// # Ok(())
235/// # }
236/// ```
237#[derive(Clone, Debug, Hash, PartialEq, Eq)]
238pub struct Shortcut {
239    pub modifiers: ModifierList,
240    pub key: Key,
241}
242
243impl FromStr for Shortcut {
244    type Err = ParseError;
245
246    fn from_str(s: &str) -> Result<Self, Self::Err> {
247        if let Some((modifiers, key)) = s.split_once('-') {
248            Ok(Shortcut {
249                modifiers: modifiers.parse()?,
250                key: key.parse()?,
251            })
252        } else {
253            Ok(Shortcut {
254                modifiers: ModifierList::default(),
255                key: s.parse()?,
256            })
257        }
258    }
259}
260
261impl Display for Shortcut {
262    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
263        if self.modifiers.is_empty() {
264            write!(f, "{}", self.key)
265        } else {
266            write!(f, "{}-{}", self.modifiers, self.key)
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use crate::{Key, Modifier, ModifierList, Shortcut};
274    use test_case::test_case;
275
276    #[test_case("KeyP", Shortcut::new(& [], Key::KeyP))]
277    #[test_case("<Ctrl>-KeyP", Shortcut::new(& [Modifier::Ctrl], Key::KeyP))]
278    #[test_case("<LeftAlt><LeftCtrl>-KeyLeft", Shortcut::new(& [Modifier::LeftCtrl, Modifier::LeftAlt], Key::KeyLeft))]
279    fn shortcut_parse_display_test(s: &str, shortcut: Shortcut) {
280        assert_eq!(s, format!("{}", shortcut));
281
282        assert_eq!(shortcut, s.parse().unwrap());
283    }
284
285    #[test_case(& [Modifier::Ctrl])]
286    #[test_case(& [Modifier::LeftAlt, Modifier::LeftCtrl])]
287    #[test_case(& [Modifier::Shift, Modifier::Meta])]
288    fn test_modifier_list(modifiers: &[Modifier]) {
289        assert_eq!(
290            modifiers.to_vec(),
291            ModifierList::new(modifiers).modifiers().collect::<Vec<_>>()
292        )
293    }
294}
295
296impl Shortcut {
297    pub fn new(modifiers: &[Modifier], key: Key) -> Self {
298        Shortcut {
299            modifiers: ModifierList::new(modifiers),
300            key,
301        }
302    }
303
304    pub fn identifier(&self) -> String {
305        self.to_string().replace(['<', '>'], "").replace('-', "_")
306    }
307}
308
309impl Shortcut {
310    pub fn is_triggered(&self, active_keys: &HashSet<Key>) -> bool {
311        let desired_mask = self.modifiers.mask();
312        let pressed_mask = active_keys
313            .iter()
314            .fold(0, |mask, key| mask | Modifier::mask_from_key(*key));
315
316        let desired_presses = desired_mask & pressed_mask;
317        let modifiers_match = (desired_presses == pressed_mask)
318            && (desired_presses.count_ones() == self.modifiers.len());
319
320        modifiers_match && active_keys.contains(&self.key)
321    }
322}
323
324#[cfg(test)]
325mod triggered_tests {
326    use crate::{Key, Shortcut};
327    use test_case::test_case;
328
329    #[test_case("<Ctrl>-KeyP", & [] => false)]
330    #[test_case("<Ctrl>-KeyP", & [Key::KeyLeftCtrl, Key::KeyP] => true)]
331    #[test_case("<Ctrl>-KeyP", & [Key::KeyRightCtrl, Key::KeyP] => true)]
332    #[test_case("<LeftCtrl>-KeyP", & [Key::KeyLeftCtrl, Key::KeyP] => true)]
333    #[test_case("<LeftCtrl>-KeyP", & [Key::KeyRightCtrl, Key::KeyP] => false)]
334    #[test_case("<Ctrl>-KeyP", & [Key::KeyLeftCtrl, Key::KeyLeftAlt, Key::KeyP] => false)]
335    #[test_case("<LeftCtrl><LeftAlt>-KeyLeft", & [] => false)]
336    #[test_case("<LeftCtrl><LeftAlt>-KeyLeft", & [Key::KeyLeft] => false)]
337    #[test_case("<LeftCtrl><LeftAlt>-KeyLeft", & [Key::KeyLeftCtrl, Key::KeyLeft] => false)]
338    #[test_case("<LeftCtrl><LeftAlt>-KeyLeft", & [Key::KeyLeftCtrl, Key::KeyLeftAlt] => false)]
339    #[test_case("<LeftCtrl><LeftAlt>-KeyLeft", & [Key::KeyLeftCtrl, Key::KeyLeftAlt, Key::KeyRight] => false)]
340    #[test_case("<LeftCtrl><LeftAlt>-KeyLeft", & [Key::KeyLeftCtrl, Key::KeyLeftAlt, Key::KeyLeft] => true)]
341    #[test_case("<LeftCtrl><LeftAlt>-KeyLeft", & [Key::KeyLeftCtrl, Key::KeyRightAlt, Key::KeyLeft] => false)]
342    #[test_case("<Ctrl><Alt>-KeyLeft", & [Key::KeyLeftCtrl, Key::KeyRightAlt, Key::KeyLeft] => true)]
343    fn shortcut_triggered(s: &str, keys: &[Key]) -> bool {
344        let shortcut: Shortcut = s.parse().unwrap();
345        shortcut.is_triggered(&keys.iter().copied().collect())
346    }
347}
348
349/// Whether the shortcut was pressed or released
350#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
351pub enum ShortcutState {
352    Pressed,
353    Released,
354}
355
356impl ShortcutState {
357    pub fn as_str(&self) -> &'static str {
358        match self {
359            ShortcutState::Pressed => "pressed",
360            ShortcutState::Released => "released",
361        }
362    }
363}
364
365impl Display for ShortcutState {
366    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
367        write!(f, "{}", self.as_str())
368    }
369}
370
371/// Event emitted when a shortcut is pressed or released.
372#[derive(Debug, Clone)]
373pub struct ShortcutEvent {
374    pub shortcut: Shortcut,
375    pub state: ShortcutState,
376}