flake + ci rework

This commit is contained in:
Robin Appelman 2025-06-09 17:07:18 +02:00
commit 6e13b8e412
10 changed files with 178 additions and 165 deletions

28
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,28 @@
name: "CI"
on:
pull_request:
push:
jobs:
checks:
runs-on: nix
steps:
- uses: actions/checkout@v4
- uses: https://codeberg.org/icewind/attic-action@v1
with:
name: link
instance: https://cache.icewind.link
authToken: "${{ secrets.ATTIC_TOKEN }}"
- run: nix flake check --keep-going
semver:
runs-on: nix
needs: checks
steps:
- uses: actions/checkout@v4
- uses: https://codeberg.org/icewind/attic-action@v1
with:
name: ci
instance: https://cache.icewind.me
authToken: "${{ secrets.ATTIC_TOKEN }}"
- run: nix run .#semver-checks

View file

@ -1,44 +0,0 @@
name: "CI"
on:
pull_request:
push:
jobs:
check:
runs-on: ubuntu-latest
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 .#check
clippy:
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 .#clippy
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

20
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "aho-corasick"
@ -86,7 +86,7 @@ dependencies = [
[[package]]
name = "evdev-shortcut"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"async-stream",
"evdev",
@ -97,6 +97,7 @@ dependencies = [
"test-case",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
@ -336,9 +337,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.9"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
@ -579,6 +580,17 @@ dependencies = [
"syn 2.0.18",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml_datetime"
version = "0.6.2"

View file

@ -6,6 +6,7 @@ edition = "2021"
description = "Global shortcuts using evdev"
license = "MIT OR Apache-2.0"
repository = "https://github.com/icewind1991/evdev-shortcut"
rust-version = "1.78.0"
[dependencies]
evdev = { version = "0.12.1", optional = true, features = ["tokio"] }
@ -20,7 +21,13 @@ tracing = "0.1.37"
test-case = "3.1.0"
glob = "0.3.1"
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1.17"
[features]
listener = ["evdev", "futures", "async-stream"]
default = ["listener"]
[[example]]
name = "listen"
path = "examples/listen.rs"
required-features = ["listener"]

View file

@ -29,5 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```
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.
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.

View file

@ -1,5 +1,6 @@
use std::path::PathBuf;
use futures::{pin_mut, StreamExt};
use std::pin::pin;
use tokio_stream::StreamExt;
use glob::GlobError;
use evdev_shortcut::{Key, Modifier, Shortcut, ShortcutListener};
@ -11,9 +12,7 @@ async fn main() {
let devices =
glob::glob("/dev/input/by-id/*-kbd").unwrap().collect::<Result<Vec<PathBuf>, GlobError>>().unwrap();
let stream = listener.listen(&devices).unwrap();
pin_mut!(stream);
let mut stream = pin!(listener.listen(&devices).unwrap());
while let Some(event) = stream.next().await {
println!("{} {}", event.shortcut, event.state);

95
flake.lock generated
View file

@ -1,64 +1,98 @@
{
"nodes": {
"naersk": {
"crane": {
"locked": {
"lastModified": 1742394900,
"narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=",
"owner": "ipetkov",
"repo": "crane",
"rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flakelight": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1686242667,
"narHash": "sha256-I7Kwp06WX/9E+rEND1i1wjdKQQm3XiDxYOyNK9fuJu0=",
"owner": "icewind1991",
"repo": "naersk",
"rev": "6d245a3bbb2ee31ec726bb57b9a8b206302e7110",
"lastModified": 1749473391,
"narHash": "sha256-NkkS2d7OvXL9VJS1pae+txd43vUIMWtxlIBdsKCRtYg=",
"owner": "nix-community",
"repo": "flakelight",
"rev": "22c8488d41c4e4678d32538f855bc05b3ba5142e",
"type": "github"
},
"original": {
"owner": "icewind1991",
"repo": "naersk",
"rev": "6d245a3bbb2ee31ec726bb57b9a8b206302e7110",
"owner": "nix-community",
"repo": "flakelight",
"type": "github"
}
},
"mill-scale": {
"inputs": {
"crane": "crane",
"flakelight": [
"flakelight"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1749481047,
"narHash": "sha256-5VnLJdD91sTcJNvCaxH5r46Yl+RmyCcLftScxkEqP3k=",
"ref": "refs/heads/main",
"rev": "642a7528ddea74bbe649913c5c3bb630eea02ecb",
"revCount": 52,
"type": "git",
"url": "https://codeberg.org/icewind/mill-scale"
},
"original": {
"type": "git",
"url": "https://codeberg.org/icewind/mill-scale"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1686059680,
"narHash": "sha256-sp0WlCIeVczzB0G8f8iyRg3IYW7KG31mI66z7HIZwrI=",
"lastModified": 1749237914,
"narHash": "sha256-N5waoqWt8aMr/MykZjSErOokYH6rOsMMXu3UOVH5kiw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a558f7ac29f50c4b937fb5c102f587678ae1c9fb",
"rev": "70c74b02eac46f4e4aa071e45a6189ce0f6d9265",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-23.05",
"ref": "nixos-25.05",
"type": "indirect"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"utils": "utils"
"flakelight": "flakelight",
"mill-scale": "mill-scale",
"nixpkgs": "nixpkgs"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"utils"
],
"nixpkgs": [
"mill-scale",
"flakelight",
"nixpkgs"
]
},
"locked": {
"lastModified": 1686191569,
"narHash": "sha256-8ey5FOXNms9piFGTn6vJeAQmSKk+NL7GTMSoVttsNTs=",
"lastModified": 1742697269,
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b4b71458b92294e8f1c3a112d972e3cff8a2ab71",
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
"type": "github"
},
"original": {
@ -66,21 +100,6 @@
"repo": "rust-overlay",
"type": "github"
}
},
"utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",

View file

@ -1,48 +1,15 @@
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-23.05";
utils.url = "github:numtide/flake-utils";
naersk.url = "github:icewind1991/naersk?rev=6d245a3bbb2ee31ec726bb57b9a8b206302e7110";
naersk.inputs.nixpkgs.follows = "nixpkgs";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
rust-overlay.inputs.flake-utils.follows = "utils";
nixpkgs.url = "nixpkgs/nixos-25.05";
flakelight = {
url = "github:nix-community/flakelight";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {
self,
nixpkgs,
utils,
naersk,
rust-overlay,
}:
utils.lib.eachDefaultSystem (system: let
overlays = [ (import rust-overlay) ];
pkgs = (import nixpkgs) {
inherit system overlays;
mill-scale = {
url = "git+https://codeberg.org/icewind/mill-scale";
inputs.flakelight.follows = "flakelight";
};
lib = pkgs.lib;
naersk' = pkgs.callPackage naersk {};
src = lib.sources.sourceByRegex (lib.cleanSource ./.) ["Cargo.*" "(src)(/.*)?"];
nearskOpt = {
pname = "evdev-shortcut";
root = src;
};
in rec {
packages = {
check = naersk'.buildPackage (nearskOpt // {
mode = "check";
});
clippy = naersk'.buildPackage (nearskOpt // {
mode = "clippy";
});
test = naersk'.buildPackage (nearskOpt // {
mode = "test";
});
};
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [rustc cargo bacon cargo-edit cargo-outdated clippy cargo-audit cargo-msrv cargo-fuzz];
};
});
outputs = {mill-scale, ...}:
mill-scale ./. {};
}

View file

@ -11,9 +11,12 @@
//! ```rust,no_run
//! # use std::path::PathBuf;
//! # use glob::GlobError;
//! # use evdev_shortcut::{ShortcutListener, Shortcut, Modifier, Key};
//! # #[cfg(feature = "listener")]
//! # use evdev_shortcut::{ShortcutListener};
//! # use evdev_shortcut::{Shortcut, Modifier, Key};
//! # use tokio::pin;
//! # use futures::stream::StreamExt;
//! # use tokio_stream::StreamExt;
//! # #[cfg(feature = "listener")]
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let listener = ShortcutListener::new();
@ -30,6 +33,10 @@
//! }
//! # Ok(())
//! # }
//! # #[cfg(not(feature = "listener"))]
//! # fn main() {
//! # // placeholder so we can build the doc example without default features
//! # }
//! ```
pub use keycodes::Key;
@ -134,21 +141,26 @@ pub struct ModifierList(u8);
impl ModifierList {
pub fn new(modifiers: &[Modifier]) -> Self {
ModifierList(modifiers
ModifierList(
modifiers
.iter()
.fold(0, |mask, modifier| mask | modifier.mask()))
.fold(0, |mask, modifier| mask | modifier.mask()),
)
}
pub fn mask(&self) -> u8 {
self.0
}
pub fn modifiers(&self) -> impl Iterator<Item=Modifier> {
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() {
if combined != modifier
&& combined.mask() & modifier.mask() == modifier.mask()
&& combined.mask() & mask == combined.mask()
{
return false;
}
}
@ -178,7 +190,8 @@ impl FromStr for ModifierList {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let modifiers = s.split('>')
let modifiers = s
.split('>')
.filter(|part| !part.is_empty())
.map(|part| {
if !part.starts_with('<') {
@ -251,8 +264,8 @@ impl Display for Shortcut {
#[cfg(test)]
mod tests {
use test_case::test_case;
use crate::{Key, Modifier, ModifierList, Shortcut};
use test_case::test_case;
#[test_case("KeyP", Shortcut::new(& [], Key::KeyP))]
#[test_case("<Ctrl>-KeyP", Shortcut::new(& [Modifier::Ctrl], Key::KeyP))]
@ -267,7 +280,10 @@ mod tests {
#[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<_>>())
assert_eq!(
modifiers.to_vec(),
ModifierList::new(modifiers).modifiers().collect::<Vec<_>>()
)
}
}
@ -280,9 +296,7 @@ impl Shortcut {
}
pub fn identifier(&self) -> String {
self.to_string()
.replace(['<', '>'], "")
.replace('-', "_")
self.to_string().replace(['<', '>'], "").replace('-', "_")
}
}
@ -322,7 +336,7 @@ mod triggered_tests {
#[test_case("<Ctrl><Alt>-KeyLeft", & [Key::KeyLeftCtrl, Key::KeyRightAlt, Key::KeyLeft] => true)]
fn shortcut_triggered(s: &str, keys: &[Key]) -> bool {
let shortcut: Shortcut = s.parse().unwrap();
shortcut.is_triggered(&keys.into_iter().copied().collect())
shortcut.is_triggered(&keys.iter().copied().collect())
}
}

View file

@ -1,14 +1,14 @@
use crate::{DeviceOpenError, Key, Shortcut, ShortcutEvent, ShortcutState};
use async_stream::stream;
use evdev::Device;
use futures::pin_mut;
use futures::stream::iter;
use futures::{Stream, StreamExt};
use std::collections::HashSet;
use std::convert::TryFrom;
use std::sync::{Arc, Mutex};
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};
use tracing::{debug, trace, info};
use std::sync::{Arc, Mutex};
use tracing::{debug, info, trace};
/// A listener for shortcut events
///
@ -42,19 +42,28 @@ impl ShortcutListener {
/// 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 devices = devices
.iter()
.map(|path| {
let path = path.as_ref();
let res = Device::open(path).map_err(|_| DeviceOpenError { device: path.into() });
let res = Device::open(path).map_err(|_| DeviceOpenError {
device: path.into(),
});
debug!(device = ?path, success = res.is_ok(), "opening input device");
res
})
.collect::<Result<Vec<Device>, DeviceOpenError>>()?;
let events = iter(devices.into_iter().flat_map(|device| device.into_event_stream()))
let events = iter(
devices
.into_iter()
.flat_map(|device| device.into_event_stream()),
)
.flatten();
Ok(stream! {