mirror of
https://codeberg.org/icewind/evdev-shortcut.git
synced 2026-06-03 10:04:11 +02:00
update to streams
This commit is contained in:
parent
aa5613a57c
commit
07d9edac20
10 changed files with 802 additions and 271 deletions
128
src/lib.rs
128
src/lib.rs
|
|
@ -3,20 +3,22 @@ use parse_display::{Display, FromStr, ParseError};
|
|||
use std::collections::HashSet;
|
||||
use std::fmt::{self, Display};
|
||||
use std::str::FromStr;
|
||||
use err_derive::Error;
|
||||
use thiserror::Error;
|
||||
|
||||
mod keycodes;
|
||||
|
||||
#[cfg(feature = "listener")]
|
||||
mod listener;
|
||||
|
||||
#[cfg(feature = "listener")]
|
||||
pub use listener::ShortcutListener;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
#[error(display = "Failed to open device")]
|
||||
#[error("Failed to open device")]
|
||||
pub struct DeviceOpenError;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Display, FromStr)]
|
||||
#[repr(u8)]
|
||||
pub enum Modifier {
|
||||
Alt,
|
||||
LeftAlt,
|
||||
|
|
@ -32,8 +34,30 @@ pub enum Modifier {
|
|||
RightMeta,
|
||||
}
|
||||
|
||||
const ALL_MODIFIERS: &[Modifier] = &[
|
||||
Modifier::Alt,
|
||||
Modifier::LeftAlt,
|
||||
Modifier::RightAlt,
|
||||
Modifier::Ctrl,
|
||||
Modifier::LeftCtrl,
|
||||
Modifier::RightCtrl,
|
||||
Modifier::Shift,
|
||||
Modifier::LeftShift,
|
||||
Modifier::RightShift,
|
||||
Modifier::Meta,
|
||||
Modifier::LeftMeta,
|
||||
Modifier::RightMeta,
|
||||
];
|
||||
|
||||
const COMBINED_MODIFIERS: &[Modifier] = &[
|
||||
Modifier::Alt,
|
||||
Modifier::Ctrl,
|
||||
Modifier::Shift,
|
||||
Modifier::Meta,
|
||||
];
|
||||
|
||||
impl Modifier {
|
||||
pub fn as_mask(&self) -> u8 {
|
||||
pub fn mask(&self) -> u8 {
|
||||
match self {
|
||||
Modifier::Alt => 0b00000011,
|
||||
Modifier::LeftAlt => 0b00000001,
|
||||
|
|
@ -65,20 +89,45 @@ impl Modifier {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct ModifierList(Vec<Modifier>);
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
|
||||
pub struct ModifierList(u8);
|
||||
|
||||
impl ModifierList {
|
||||
pub fn as_mask(&self) -> u8 {
|
||||
self.0
|
||||
pub fn new(modifiers: &[Modifier]) -> Self {
|
||||
ModifierList(modifiers
|
||||
.iter()
|
||||
.fold(0, |mask, modifier| mask | modifier.as_mask())
|
||||
.fold(0, |mask, modifier| mask | modifier.mask()))
|
||||
}
|
||||
|
||||
pub fn mask(&self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn modifiers(&self) -> impl Iterator<Item=Modifier> {
|
||||
let mask = self.mask();
|
||||
ALL_MODIFIERS.iter().copied().filter(move |modifier| {
|
||||
for combined in COMBINED_MODIFIERS {
|
||||
// if <Ctrl> is enabled, don't emit <LeftCtrl> and <RightCtrl>
|
||||
if combined != modifier && combined.mask() & modifier.mask() == modifier.mask() && combined.mask() & mask == combined.mask() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
modifier.mask() & mask == modifier.mask()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn len(&self) -> u32 {
|
||||
self.modifiers().count() as u32
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.mask() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ModifierList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for modifier in self.0.iter() {
|
||||
for modifier in self.modifiers() {
|
||||
write!(f, "<{}>", modifier)?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -89,18 +138,17 @@ impl FromStr for ModifierList {
|
|||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ModifierList(
|
||||
s.split('>')
|
||||
.filter(|part| !part.is_empty())
|
||||
.map(|part| {
|
||||
if !part.starts_with('<') {
|
||||
Err(ParseError::with_message("Invalid modifier"))
|
||||
} else {
|
||||
Ok(part[1..].parse::<Modifier>()?)
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<Modifier>, ParseError>>()?,
|
||||
))
|
||||
let modifiers = s.split('>')
|
||||
.filter(|part| !part.is_empty())
|
||||
.map(|part| {
|
||||
if !part.starts_with('<') {
|
||||
Err(ParseError::with_message("Invalid modifier"))
|
||||
} else {
|
||||
Ok(part[1..].parse::<Modifier>()?)
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<Modifier>, ParseError>>()?;
|
||||
Ok(ModifierList::new(&modifiers))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,44 +161,50 @@ pub struct Shortcut {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::keyboard::{Key, Modifier, Shortcut};
|
||||
use test_case::test_case;
|
||||
use crate::{Key, Modifier, ModifierList, Shortcut};
|
||||
|
||||
#[test_case("<Ctrl>-KeyP", Shortcut::new(vec ! [Modifier::Ctrl], Key::KeyP))]
|
||||
#[test_case("<LeftCtrl><LeftAlt>-KeyLeft", Shortcut::new(vec ! [Modifier::LeftCtrl, Modifier::LeftAlt], Key::KeyLeft))]
|
||||
#[test_case("<Ctrl>-KeyP", Shortcut::new(& [Modifier::Ctrl], Key::KeyP))]
|
||||
#[test_case("<LeftAlt><LeftCtrl>-KeyLeft", Shortcut::new(& [Modifier::LeftCtrl, Modifier::LeftAlt], Key::KeyLeft))]
|
||||
fn shortcut_parse_display_test(s: &str, shortcut: Shortcut) {
|
||||
assert_eq!(s, format!("{}", shortcut));
|
||||
|
||||
assert_eq!(shortcut, s.parse().unwrap());
|
||||
}
|
||||
|
||||
#[test_case(& [Modifier::Ctrl])]
|
||||
#[test_case(& [Modifier::LeftAlt, Modifier::LeftCtrl])]
|
||||
#[test_case(& [Modifier::Shift, Modifier::Meta])]
|
||||
fn test_modifier_list(modifiers: &[Modifier]) {
|
||||
assert_eq!(modifiers.to_vec(), ModifierList::new(modifiers).modifiers().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
impl Shortcut {
|
||||
pub fn new(modifiers: Vec<Modifier>, key: Key) -> Self {
|
||||
pub fn new(modifiers: &[Modifier], key: Key) -> Self {
|
||||
Shortcut {
|
||||
modifiers: ModifierList(modifiers),
|
||||
modifiers: ModifierList::new(modifiers),
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn identifier(&self) -> String {
|
||||
self.to_string()
|
||||
.replace('<', "")
|
||||
.replace('>', "")
|
||||
.replace(['<', '>'], "")
|
||||
.replace('-', "_")
|
||||
}
|
||||
}
|
||||
|
||||
impl Shortcut {
|
||||
pub fn is_triggered(&self, active_keys: &HashSet<Key>) -> bool {
|
||||
let desired_mask = self.modifiers.as_mask();
|
||||
let desired_mask = self.modifiers.mask();
|
||||
let pressed_mask = active_keys
|
||||
.iter()
|
||||
.fold(0, |mask, key| mask | Modifier::mask_from_key(*key));
|
||||
|
||||
let desired_presses = desired_mask & pressed_mask;
|
||||
let modifiers_match = (desired_presses == pressed_mask)
|
||||
&& (desired_presses.count_ones() == self.modifiers.0.len() as u32);
|
||||
&& (desired_presses.count_ones() == self.modifiers.len());
|
||||
|
||||
modifiers_match && active_keys.contains(&self.key)
|
||||
}
|
||||
|
|
@ -158,7 +212,7 @@ impl Shortcut {
|
|||
|
||||
#[cfg(test)]
|
||||
mod triggered_tests {
|
||||
use crate::keyboard::{Key, Shortcut};
|
||||
use crate::{Key, Shortcut};
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case("<Ctrl>-KeyP", & [] => false)]
|
||||
|
|
@ -179,4 +233,16 @@ mod triggered_tests {
|
|||
let shortcut: Shortcut = s.parse().unwrap();
|
||||
shortcut.is_triggered(&keys.into_iter().copied().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ShortcutState {
|
||||
Pressed,
|
||||
Released,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShortcutEvent {
|
||||
pub shortcut: Shortcut,
|
||||
pub state: ShortcutState,
|
||||
}
|
||||
|
|
@ -1,68 +1,71 @@
|
|||
use evdev::Device;
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::sync::mpsc::{channel, Receiver};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use crate::{Shortcut, DeviceOpenError, Key};
|
||||
use crate::{Shortcut, DeviceOpenError, Key, ShortcutEvent, ShortcutState};
|
||||
use std::path::Path;
|
||||
use async_stream::stream;
|
||||
use futures::pin_mut;
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::stream::{iter};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ShortcutListener {
|
||||
shortcuts: Arc<Mutex<HashSet<Shortcut>>>,
|
||||
}
|
||||
|
||||
impl ShortcutListener {
|
||||
pub fn new() -> Self {
|
||||
ShortcutListener {
|
||||
shortcuts: Arc::default(),
|
||||
}
|
||||
ShortcutListener::default()
|
||||
}
|
||||
|
||||
pub fn listen<P: AsRef<Path>>(&self, devices: &[P]) -> Result<Receiver<Shortcut>, DeviceOpenError> {
|
||||
let mut devices = devices
|
||||
.iter()
|
||||
.map(|path| Ok(Device::open(path).map_err(|_| DeviceOpenError)?))
|
||||
.collect::<Result<Vec<Device>, DeviceOpenError>>()?;
|
||||
|
||||
let (tx, rx) = channel();
|
||||
|
||||
pub fn listen<P: AsRef<Path>>(&self, devices: &[P]) -> Result<impl Stream<Item=ShortcutEvent>, DeviceOpenError> {
|
||||
let shortcuts = self.shortcuts.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let devices = devices
|
||||
.iter()
|
||||
.map(|path| Device::open(path).map_err(|_| DeviceOpenError))
|
||||
.collect::<Result<Vec<Device>, DeviceOpenError>>()?;
|
||||
let events = iter(devices.into_iter().flat_map(|device| device.into_event_stream()))
|
||||
.flatten();
|
||||
|
||||
Ok(stream! {
|
||||
let mut active_keys = HashSet::new();
|
||||
let mut pressed_shortcuts = HashSet::new();
|
||||
|
||||
loop {
|
||||
let mut got_event = false;
|
||||
pin_mut!(events);
|
||||
|
||||
let events = devices
|
||||
.iter_mut()
|
||||
.flat_map(|device| device.events().unwrap());
|
||||
while let Some(Ok(event)) = events.next().await {
|
||||
// dbg!(&event);
|
||||
if let Ok(key) = Key::try_from(event.code()) {
|
||||
match event.value() {
|
||||
1 => active_keys.insert(key),
|
||||
0 => active_keys.remove(&key),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
for ev in events {
|
||||
got_event = true;
|
||||
let shortcuts: Vec<_> = shortcuts.lock().unwrap().iter().cloned().collect();
|
||||
|
||||
if let Ok(key) = Key::try_from(ev.code) {
|
||||
match ev.value {
|
||||
1 => active_keys.insert(key),
|
||||
0 => active_keys.remove(&key),
|
||||
_ => false,
|
||||
for shortcut in shortcuts {
|
||||
let is_triggered = shortcut.is_triggered(&active_keys);
|
||||
let was_triggered = pressed_shortcuts.contains(&shortcut);
|
||||
if is_triggered && !was_triggered {
|
||||
pressed_shortcuts.insert(shortcut.clone());
|
||||
yield ShortcutEvent {
|
||||
shortcut,
|
||||
state: ShortcutState::Pressed,
|
||||
};
|
||||
} else if !is_triggered && was_triggered {
|
||||
pressed_shortcuts.remove(&shortcut);
|
||||
yield ShortcutEvent {
|
||||
shortcut,
|
||||
state: ShortcutState::Released,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if got_event {
|
||||
for shortcut in shortcuts.lock().unwrap().iter() {
|
||||
if shortcut.is_triggered(&active_keys) {
|
||||
tx.send(shortcut.clone()).unwrap()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(rx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add(&self, shortcut: Shortcut) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue