mirror of
https://codeberg.org/icewind/evdev-shortcut.git
synced 2026-06-03 10:04:11 +02:00
docs
This commit is contained in:
parent
7f555ba931
commit
025ee6a620
6 changed files with 123 additions and 21 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "evdev-shortcut"
|
name = "evdev-shortcut"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
authors = ["Robin Appelman <robin@icewind.nl>"]
|
authors = ["Robin Appelman <robin@icewind.nl>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Global shortcuts using evdev"
|
description = "Global shortcuts using evdev"
|
||||||
|
|
|
||||||
33
README.md
33
README.md
|
|
@ -5,22 +5,29 @@ Global shortcuts using evdev
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use glob::GlobError;
|
||||||
|
use evdev_shortcut::{ShortcutListener, Shortcut, Modifier, Key};
|
||||||
|
use tokio::pin;
|
||||||
|
use futures::stream::StreamExt;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let listener = ShortcutListener::new();
|
let listener = ShortcutListener::new();
|
||||||
listener.add(Shortcut::new(&[Modifier::Meta], Key::KeyN));
|
listener.add(Shortcut::new(&[Modifier::Meta], Key::KeyN));
|
||||||
|
|
||||||
let devices =
|
let devices =
|
||||||
glob::glob("/dev/input/by-id/*-kbd").unwrap().collect::<Result<Vec<PathBuf>, GlobError>>().unwrap();
|
glob::glob("/dev/input/by-id/*-kbd")?.collect::<Result<Vec<PathBuf>, GlobError>>()?;
|
||||||
|
|
||||||
let stream = listener.listen(&devices).unwrap();
|
let stream = listener.listen(&devices)?;
|
||||||
|
pin!(stream);
|
||||||
pin_mut!(stream);
|
|
||||||
|
while let Some(event) = stream.next().await {
|
||||||
while let Some(shortcut) = stream.next().await {
|
println!("{} {}", event.shortcut, event.state);
|
||||||
dbg!(shortcut);
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that raw access to evdev devices is a privileged operation and usually requires root.
|
Note that raw access to evdev devices is a privileged operation and usually requires running with elevated privileges.
|
||||||
|
See [shortcutd](https://github.com/icewind1991/shortcutd) for a solution to running the elevated input handling in a separate process.
|
||||||
|
|
@ -15,7 +15,7 @@ async fn main() {
|
||||||
|
|
||||||
pin_mut!(stream);
|
pin_mut!(stream);
|
||||||
|
|
||||||
while let Some(shortcut) = stream.next().await {
|
while let Some(event) = stream.next().await {
|
||||||
dbg!(shortcut);
|
println!("{} {}", event.shortcut, event.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use parse_display::{Display, FromStr};
|
use parse_display::{Display, FromStr};
|
||||||
|
|
||||||
|
/// evdev keycode
|
||||||
#[derive(Copy, Clone, Debug, TryFromPrimitive, PartialEq, Eq, Hash, Display, FromStr)]
|
#[derive(Copy, Clone, Debug, TryFromPrimitive, PartialEq, Eq, Hash, Display, FromStr)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
|
|
|
||||||
76
src/lib.rs
76
src/lib.rs
|
|
@ -1,7 +1,42 @@
|
||||||
|
//! Global keyboard shortcuts using evdev.
|
||||||
|
//!
|
||||||
|
//! By connecting to the input devices directly with evdev the shortcuts can work regardless of the environment,
|
||||||
|
//! they will work under X11, wayland and in the terminal.
|
||||||
|
//!
|
||||||
|
//! This does come at the cost of having to run the program with elevated permissions.
|
||||||
|
//! See [shortcutd](https://docs.rs/shortcutd/latest/shortcutd/) for a solution to running the elevated input handling in a separate process.
|
||||||
|
//!
|
||||||
|
//! Example:
|
||||||
|
//!
|
||||||
|
//! ```rust,no_run
|
||||||
|
//! # use std::path::PathBuf;
|
||||||
|
//! # use glob::GlobError;
|
||||||
|
//! # use evdev_shortcut::{ShortcutListener, Shortcut, Modifier, Key};
|
||||||
|
//! # use tokio::pin;
|
||||||
|
//! # use futures::stream::StreamExt;
|
||||||
|
//! # #[tokio::main]
|
||||||
|
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let listener = ShortcutListener::new();
|
||||||
|
//! listener.add(Shortcut::new(&[Modifier::Meta], Key::KeyN));
|
||||||
|
//!
|
||||||
|
//! let devices =
|
||||||
|
//! glob::glob("/dev/input/by-id/*-kbd")?.collect::<Result<Vec<PathBuf>, GlobError>>()?;
|
||||||
|
//!
|
||||||
|
//! let stream = listener.listen(&devices)?;
|
||||||
|
//! pin!(stream);
|
||||||
|
//!
|
||||||
|
//! while let Some(event) = stream.next().await {
|
||||||
|
//! println!("{} {}", event.shortcut, event.state);
|
||||||
|
//! }
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
pub use keycodes::Key;
|
pub use keycodes::Key;
|
||||||
use parse_display::{Display, FromStr, ParseError};
|
use parse_display::{Display, FromStr, ParseError};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt::{self, Debug, Display, Formatter};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -13,10 +48,14 @@ mod listener;
|
||||||
#[cfg(feature = "listener")]
|
#[cfg(feature = "listener")]
|
||||||
pub use listener::ShortcutListener;
|
pub use listener::ShortcutListener;
|
||||||
|
|
||||||
|
/// Error emitted when an input device can't be opened
|
||||||
#[derive(Debug, Clone, Error)]
|
#[derive(Debug, Clone, Error)]
|
||||||
#[error("Failed to open device")]
|
#[error("Failed to open device {device:?}")]
|
||||||
pub struct DeviceOpenError;
|
pub struct DeviceOpenError {
|
||||||
|
pub device: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifier key for shortcuts
|
||||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Display, FromStr)]
|
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Display, FromStr)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum Modifier {
|
pub enum Modifier {
|
||||||
|
|
@ -89,6 +128,7 @@ impl Modifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set of modifier keys for shortcuts
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy, Default)]
|
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy, Default)]
|
||||||
pub struct ModifierList(u8);
|
pub struct ModifierList(u8);
|
||||||
|
|
||||||
|
|
@ -152,6 +192,29 @@ impl FromStr for ModifierList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A keyboard shortcut consisting of zero or more modifier keys and a non-modifier key
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// Create from keys:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use evdev_shortcut::{Shortcut, Modifier, Key};
|
||||||
|
/// # fn main() {
|
||||||
|
/// let shortcut = Shortcut::new(&[Modifier::Meta], Key::KeyN);
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Parse from string:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use evdev_shortcut::{Shortcut, Modifier, Key};
|
||||||
|
/// # use std::str::FromStr;
|
||||||
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let shortcut: Shortcut = "<Meta>-KeyN".parse()?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||||
pub struct Shortcut {
|
pub struct Shortcut {
|
||||||
pub modifiers: ModifierList,
|
pub modifiers: ModifierList,
|
||||||
|
|
@ -168,7 +231,6 @@ impl FromStr for Shortcut {
|
||||||
key: key.parse()?,
|
key: key.parse()?,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
Ok(Shortcut {
|
Ok(Shortcut {
|
||||||
modifiers: ModifierList::default(),
|
modifiers: ModifierList::default(),
|
||||||
key: s.parse()?,
|
key: s.parse()?,
|
||||||
|
|
@ -264,6 +326,7 @@ mod triggered_tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the shortcut was pressed or released
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
pub enum ShortcutState {
|
pub enum ShortcutState {
|
||||||
Pressed,
|
Pressed,
|
||||||
|
|
@ -279,6 +342,13 @@ impl ShortcutState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for ShortcutState {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event emitted when a shortcut is pressed or released.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ShortcutEvent {
|
pub struct ShortcutEvent {
|
||||||
pub shortcut: Shortcut,
|
pub shortcut: Shortcut,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,25 @@ use futures::{Stream, StreamExt};
|
||||||
use futures::stream::{iter};
|
use futures::stream::{iter};
|
||||||
use tracing::{debug, trace, info};
|
use tracing::{debug, trace, info};
|
||||||
|
|
||||||
|
/// A listener for shortcut events
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use std::path::PathBuf;
|
||||||
|
/// # use glob::GlobError;
|
||||||
|
/// # use evdev_shortcut::{ShortcutListener, Shortcut, Modifier, Key};
|
||||||
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let listener = ShortcutListener::new();
|
||||||
|
/// listener.add(Shortcut::new(&[Modifier::Meta], Key::KeyN));
|
||||||
|
///
|
||||||
|
/// let devices =
|
||||||
|
/// glob::glob("/dev/input/by-id/*-kbd")?.collect::<Result<Vec<PathBuf>, GlobError>>()?;
|
||||||
|
///
|
||||||
|
/// let stream = listener.listen(&devices)?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ShortcutListener {
|
pub struct ShortcutListener {
|
||||||
shortcuts: Arc<Mutex<HashSet<Shortcut>>>,
|
shortcuts: Arc<Mutex<HashSet<Shortcut>>>,
|
||||||
|
|
@ -20,14 +39,18 @@ impl ShortcutListener {
|
||||||
ShortcutListener::default()
|
ShortcutListener::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Listen for shortcuts on the provided set of input devices.
|
||||||
|
///
|
||||||
|
/// Note that you need to register shortcuts using [add](ShortcutListener::add) to get any events.
|
||||||
pub fn listen<P: AsRef<Path>>(&self, devices: &[P]) -> Result<impl Stream<Item=ShortcutEvent>, DeviceOpenError> {
|
pub fn listen<P: AsRef<Path>>(&self, devices: &[P]) -> Result<impl Stream<Item=ShortcutEvent>, DeviceOpenError> {
|
||||||
let shortcuts = self.shortcuts.clone();
|
let shortcuts = self.shortcuts.clone();
|
||||||
|
|
||||||
let devices = devices
|
let devices = devices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|path| {
|
.map(|path| {
|
||||||
let res = Device::open(path).map_err(|_| DeviceOpenError);
|
let path = path.as_ref();
|
||||||
debug!(device = ?path.as_ref(), success = res.is_ok(), "opening input device");
|
let res = Device::open(path).map_err(|_| DeviceOpenError { device: path.into() });
|
||||||
|
debug!(device = ?path, success = res.is_ok(), "opening input device");
|
||||||
res
|
res
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<Device>, DeviceOpenError>>()?;
|
.collect::<Result<Vec<Device>, DeviceOpenError>>()?;
|
||||||
|
|
@ -85,6 +108,7 @@ impl ShortcutListener {
|
||||||
self.shortcuts.lock().unwrap().remove(shortcut)
|
self.shortcuts.lock().unwrap().remove(shortcut)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a shortcut is currently being listened for
|
||||||
pub fn has(&self, shortcut: &Shortcut) -> bool {
|
pub fn has(&self, shortcut: &Shortcut) -> bool {
|
||||||
self.shortcuts.lock().unwrap().contains(shortcut)
|
self.shortcuts.lock().unwrap().contains(shortcut)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue