allow some bare shortcuts, example work

This commit is contained in:
Robin Appelman 2023-06-17 20:55:25 +02:00
commit 42237bc965
9 changed files with 232 additions and 63 deletions

View file

@ -65,16 +65,3 @@ jobs:
with: with:
name: ${{ matrix.artifact_name }}-${{ matrix.target }} name: ${{ matrix.artifact_name }}-${{ matrix.target }}
path: result/bin/${{ matrix.artifact_name }} path: result/bin/${{ matrix.artifact_name }}
test:
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
- uses: icewind1991/attic-action@v1
with:
name: ci
instance: https://cache.icewind.me
authToken: '${{ secrets.ATTIC_TOKEN }}'
- run: nix build .#test

132
Cargo.lock generated
View file

@ -11,6 +11,55 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anstream"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
[[package]]
name = "anstyle-parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.5.1" version = "0.5.1"
@ -207,6 +256,54 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed"
dependencies = [
"clap_builder",
"clap_derive",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636"
dependencies = [
"anstream",
"anstyle",
"bitflags",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.2.0" version = "2.2.0"
@ -324,9 +421,9 @@ dependencies = [
[[package]] [[package]]
name = "evdev-shortcut" name = "evdev-shortcut"
version = "0.1.1" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa8872ec4c0a6e6f5d6649384273a88a4941a0bd637a25712eadd74da2cb0748" checksum = "cb870dc02f8a7955fa4ff451bbdd8bf9362d71b245a76bbe73a4c000cf012709"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"evdev", "evdev",
@ -495,6 +592,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.2.6" version = "0.2.6"
@ -546,6 +649,18 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "is-terminal"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi 0.3.1",
"io-lifetimes",
"rustix",
"windows-sys",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -957,6 +1072,7 @@ dependencies = [
name = "shortcutd" name = "shortcutd"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap",
"evdev-shortcut", "evdev-shortcut",
"futures", "futures",
"test-case", "test-case",
@ -1029,6 +1145,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "structmeta" name = "structmeta"
version = "0.2.0" version = "0.2.0"
@ -1297,6 +1419,12 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View file

@ -11,8 +11,7 @@ The shortcutd daemon hooks into the evdev system and exposes a dbus interface fo
By separating out the code that hooks into evdev (which needs to be done as root) into a separate daemon By separating out the code that hooks into evdev (which needs to be done as root) into a separate daemon
it allows non-privileged users to hook into global shortcuts. it allows non-privileged users to hook into global shortcuts.
Protection against clients using the shortcutd daemon for a keylogger is done by only allowing shortcuts that Protection against clients using the shortcutd daemon for a keylogger is done by only allowing 3 shortcuts without modifiers to be registered at the same time.
contain at least one modifier key.
## Starting the daemon ## Starting the daemon
@ -22,33 +21,32 @@ contain at least one modifier key.
## Rust api ## Rust api
```rust ```rust
use futures::{pin_mut, StreamExt};
use shortcutd::{Shortcut, ShortcutClient}; use shortcutd::{Shortcut, ShortcutClient};
use std::error::Error; use std::error::Error;
use std::time::Duration;
fn main() -> Result<(), Box<dyn Error>> { #[tokio::main]
let mut client = ShortcutClient::new()?; async fn main() -> Result<(), Box<dyn Error>> {
let client = ShortcutClient::new().await?;
let shortcut: Shortcut = "<Ctrl><Alt>-KeyP".parse()?;
client.register(shortcut, |s| {
eprintln!("shortcut1 {}", s);
})?;
let shortcut: Shortcut = "<Ctrl><Alt>-KeyO".parse()?; let shortcut: Shortcut = "<Ctrl><Alt>-KeyO".parse()?;
client.register(shortcut, |s| { let stream = client.listen(shortcut).await?;
eprintln!("shortcut2 {}", s);
})?;
loop { pin_mut!(stream);
client.process(Duration::from_millis(1000))?;
while let Some(event) = stream.next().await {
println!("{} {}", event.shortcut, event.state.as_str());
} }
Ok(())
} }
``` ```
## D-Bus api ## D-Bus api
- register a new shortcut using the `Register` method at `nl.icewind.shortcutd`/`register` - register a new shortcut using the `Register` method at `nl.icewind.shortcutd`/`register`
- listen to the signal at the path returned from the `Register` method to get notified when the shortcut is triggered - listen to the signal at the path returned from the `Register` method to get notified when the shortcut is triggered.
A boolean parameter is provided with the signal to distinguish shortcut presses from releases.

View file

@ -11,8 +11,9 @@ path = "src/lib.rs"
[dependencies] [dependencies]
futures = "0.3.28" futures = "0.3.28"
zbus = { version = "3.13.1", features = ["tokio"], default-features = false } zbus = { version = "3.13.1", features = ["tokio"], default-features = false }
evdev-shortcut = { version = "0.1.1", default_features = false } evdev-shortcut = { version = "0.1.2", default_features = false }
[dev-dependencies] [dev-dependencies]
test-case = "3.1.0" test-case = "3.1.0"
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
clap = { version = "4.3.4", features = ["derive"] }

View file

@ -1,19 +1,26 @@
use shortcutd::{ShortcutClient}; use clap::Parser;
use std::error::Error; use evdev_shortcut::Shortcut;
use futures::pin_mut; use futures::pin_mut;
use futures::stream::iter; use futures::stream::iter;
use futures::StreamExt; use futures::StreamExt;
use shortcutd::ShortcutClient;
use std::error::Error;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Shortcut to listen to
shortcuts: Vec<Shortcut>,
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let client = ShortcutClient::new().await?; let client = ShortcutClient::new().await?;
let streams = [ let stream = iter(args.shortcuts)
Box::pin(client.register("<Ctrl>-KeyM".parse()?).await?), .then(|shortcut| async { Box::pin(client.listen(shortcut).await.unwrap()) })
Box::pin(client.register("<Ctrl><Alt>-KeyO".parse()?).await?), .flatten_unordered(None);
];
let stream = iter(streams).flatten_unordered(None);
pin_mut!(stream); pin_mut!(stream);

19
client/examples/simple.rs Normal file
View file

@ -0,0 +1,19 @@
use futures::{pin_mut, StreamExt};
use shortcutd::{Shortcut, ShortcutClient};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let client = ShortcutClient::new().await?;
let shortcut: Shortcut = "<Ctrl><Alt>-KeyO".parse()?;
let stream = client.listen(shortcut).await?;
pin_mut!(stream);
while let Some(event) = stream.next().await {
println!("{} {}", event.shortcut, event.state.as_str());
}
Ok(())
}

View file

@ -1,23 +1,22 @@
pub use evdev_shortcut::Shortcut; pub use evdev_shortcut::Shortcut;
use evdev_shortcut::{ShortcutEvent, ShortcutState}; use evdev_shortcut::{ShortcutEvent, ShortcutState};
use futures::Stream; use futures::Stream;
use zbus::{Connection, fdo};
use zbus::dbus_proxy;
use futures::StreamExt; use futures::StreamExt;
use zbus::dbus_proxy;
use zbus::{fdo, Connection};
#[dbus_proxy( #[dbus_proxy(
interface = "nl.icewind.shortcutd", interface = "nl.icewind.shortcutd",
default_service = "nl.icewind.shortcutd", default_service = "nl.icewind.shortcutd",
default_path = "/register" default_path = "/register"
)] )]
trait Register { trait Register {
async fn register(&self, shortcut: &str) -> fdo::Result<String>; async fn register(&self, shortcut: &str) -> fdo::Result<String>;
} }
#[dbus_proxy( #[dbus_proxy(
interface = "nl.icewind.shortcutd", interface = "nl.icewind.shortcutd",
default_service = "nl.icewind.shortcutd" default_service = "nl.icewind.shortcutd"
)] )]
trait ShortcutSignal { trait ShortcutSignal {
#[dbus_proxy(signal)] #[dbus_proxy(signal)]
@ -39,14 +38,17 @@ impl ShortcutClient {
ShortcutClient { connection } ShortcutClient { connection }
} }
pub async fn register( pub async fn listen(
&self, &self,
shortcut: Shortcut, shortcut: Shortcut,
) -> Result<impl Stream<Item=ShortcutEvent> + '_, zbus::Error> { ) -> Result<impl Stream<Item = ShortcutEvent> + '_, zbus::Error> {
let register = RegisterProxy::new(&self.connection).await?; let register = RegisterProxy::new(&self.connection).await?;
let path = register.register(&format!("{}", shortcut)).await?; let path = register.register(&format!("{}", shortcut)).await?;
let p = ShortcutSignalProxy::builder(&self.connection).path(path.as_str())?.build().await?; let p = ShortcutSignalProxy::builder(&self.connection)
.path(path.as_str())?
.build()
.await?;
let signals = p.receive_triggered().await?; let signals = p.receive_triggered().await?;
Ok(signals.filter_map(move |signal| { Ok(signals.filter_map(move |signal| {
@ -55,7 +57,11 @@ impl ShortcutClient {
let pressed = signal.args().ok()?.pressed; let pressed = signal.args().ok()?.pressed;
Some(ShortcutEvent { Some(ShortcutEvent {
shortcut, shortcut,
state: if pressed { ShortcutState::Pressed } else { ShortcutState::Released }, state: if pressed {
ShortcutState::Pressed
} else {
ShortcutState::Released
},
}) })
} }
})) }))

View file

@ -11,7 +11,7 @@ path = "src/server.rs"
[dependencies] [dependencies]
main_error = "0.1.0" main_error = "0.1.0"
glob = "0.3.0" glob = "0.3.0"
evdev-shortcut = "0.1.1" evdev-shortcut = "0.1.3"
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
futures = "0.3.28" futures = "0.3.28"
zbus = { version = "3.13.1", features = ["tokio"], default-features = false } zbus = { version = "3.13.1", features = ["tokio"], default-features = false }

View file

@ -1,21 +1,38 @@
use main_error::MainError;
use evdev_shortcut::{Shortcut, ShortcutEvent, ShortcutListener, ShortcutState}; use evdev_shortcut::{Shortcut, ShortcutEvent, ShortcutListener, ShortcutState};
use glob::GlobError;
use std::path::PathBuf;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use glob::GlobError;
use main_error::MainError;
use std::path::PathBuf;
use tracing::info; use tracing::info;
use zbus::{ConnectionBuilder, dbus_interface, fdo, SignalContext, ObjectServer};
use zbus::export::futures_util::pin_mut; use zbus::export::futures_util::pin_mut;
use zbus::{dbus_interface, fdo, ConnectionBuilder, ObjectServer, SignalContext};
struct Register { struct Register {
listener: ShortcutListener, listener: ShortcutListener,
bare_count: usize,
} }
const MAX_BARE: usize = 3;
#[dbus_interface(name = "nl.icewind.shortcutd")] #[dbus_interface(name = "nl.icewind.shortcutd")]
impl Register { impl Register {
async fn register(&mut self, shortcut: &str, #[zbus(object_server)] server: &ObjectServer) -> Result<String, fdo::Error> { async fn register(
&mut self,
shortcut: &str,
#[zbus(object_server)] server: &ObjectServer,
) -> Result<String, fdo::Error> {
match shortcut.parse::<Shortcut>() { match shortcut.parse::<Shortcut>() {
Ok(shortcut) => { Ok(shortcut) => {
if shortcut.modifiers.is_empty() && !self.listener.has(&shortcut) {
dbg!(&shortcut);
if self.bare_count >= MAX_BARE {
return Err(fdo::Error::InvalidArgs(format!(
"Only {} shortcuts without modifiers are allowed",
MAX_BARE
)));
}
self.bare_count += 1;
}
info!(%shortcut, "registering shortcut"); info!(%shortcut, "registering shortcut");
self.listener.add(shortcut.clone()); self.listener.add(shortcut.clone());
let path = format!("/{}", shortcut.identifier()); let path = format!("/{}", shortcut.identifier());
@ -49,6 +66,7 @@ async fn main() -> Result<(), MainError> {
let bus = Register { let bus = Register {
listener, listener,
bare_count: 0,
}; };
let conn = ConnectionBuilder::system()? let conn = ConnectionBuilder::system()?
.name("nl.icewind.shortcutd")? .name("nl.icewind.shortcutd")?
@ -63,7 +81,12 @@ async fn main() -> Result<(), MainError> {
let event: ShortcutEvent = event; let event: ShortcutEvent = event;
let identifier = format!("/{}", event.shortcut.identifier()); let identifier = format!("/{}", event.shortcut.identifier());
if let Ok(signal_interface) = server.interface::<_, ShortcutSignal>(identifier).await { if let Ok(signal_interface) = server.interface::<_, ShortcutSignal>(identifier).await {
if let Err(e) = ShortcutSignal::triggered(signal_interface.signal_context(), event.state == ShortcutState::Pressed).await { if let Err(e) = ShortcutSignal::triggered(
signal_interface.signal_context(),
event.state == ShortcutState::Pressed,
)
.await
{
eprintln!("{e:#}"); eprintln!("{e:#}");
} }
} }