diff --git a/.tangled/workflows/check.yaml b/.tangled/workflows/check.yaml
index 649cef5..d31496a 100644
--- a/.tangled/workflows/check.yaml
+++ b/.tangled/workflows/check.yaml
@@ -12,6 +12,8 @@ dependencies:
- gcc
- linux-pam
- pkg-config
+ - udev
+ - acl
steps:
- name: cargo check
@@ -33,6 +35,9 @@ steps:
Libs: -L\${libdir} -lsession
Cflags: -I\${includedir}
EOF
- export PKG_CONFIG_PATH="$(pwd)/target/pkgconfig"
+ export PKG_CONFIG_PATH="$(pwd)/target/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
cargo check --workspace --all-features
+ cargo check --manifest-path sessiond-backlight/Cargo.toml
+ cargo check --manifest-path sessiond-hooks/Cargo.toml
+ cargo check --manifest-path sessiond-uaccess/Cargo.toml
diff --git a/.tangled/workflows/clippy.yaml b/.tangled/workflows/clippy.yaml
index 89d7dbb..4865152 100644
--- a/.tangled/workflows/clippy.yaml
+++ b/.tangled/workflows/clippy.yaml
@@ -12,6 +12,8 @@ dependencies:
- gcc
- linux-pam
- pkg-config
+ - udev
+ - acl
steps:
- name: cargo clippy
@@ -34,6 +36,9 @@ steps:
Libs: -L\${libdir} -lsession
Cflags: -I\${includedir}
EOF
- export PKG_CONFIG_PATH="$(pwd)/target/pkgconfig"
+ export PKG_CONFIG_PATH="$(pwd)/target/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
cargo clippy --workspace --all-features -- -D warnings
+ cargo clippy --manifest-path sessiond-backlight/Cargo.toml -- -D warnings
+ cargo clippy --manifest-path sessiond-hooks/Cargo.toml -- -D warnings
+ cargo clippy --manifest-path sessiond-uaccess/Cargo.toml -- -D warnings
diff --git a/.tangled/workflows/fmt.yaml b/.tangled/workflows/fmt.yaml
index 2284832..6f49699 100644
--- a/.tangled/workflows/fmt.yaml
+++ b/.tangled/workflows/fmt.yaml
@@ -25,3 +25,6 @@ steps:
rustup default stable
rustup component add rustfmt
cargo fmt --check
+ cargo fmt --manifest-path sessiond-backlight/Cargo.toml --check
+ cargo fmt --manifest-path sessiond-hooks/Cargo.toml --check
+ cargo fmt --manifest-path sessiond-uaccess/Cargo.toml --check
diff --git a/.tangled/workflows/test.yaml b/.tangled/workflows/test.yaml
index 730858e..8b95f41 100644
--- a/.tangled/workflows/test.yaml
+++ b/.tangled/workflows/test.yaml
@@ -13,14 +13,19 @@ dependencies:
- linux-pam
- pkg-config
- nix
+ - udev
+ - acl
steps:
- name: cargo test
command: |
rustup toolchain install stable
rustup default stable
- cargo build --package daemon
- cargo test --package daemon
+ cargo build --package sessiond
+ cargo test --package sessiond
+ cargo test --manifest-path sessiond-backlight/Cargo.toml
+ cargo test --manifest-path sessiond-hooks/Cargo.toml
+ cargo test --manifest-path sessiond-uaccess/Cargo.toml
- name: finix vm tests
command: |
diff --git a/Cargo.lock b/Cargo.lock
index 3c47d0c..cda1a83 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "aho-corasick"
-version = "1.1.4"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
@@ -63,9 +63,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.103"
+version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arrayvec"
@@ -166,7 +166,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -198,13 +198,13 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.89"
+version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
@@ -224,9 +224,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bevy_ecs"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4654b9b3535fa7813d2878a50e1b5ad80a361dc1c913b96ded57667f65d70a01"
+checksum = "a350b2e4c2e47f7903d688affb8a723b31bca63af9789f1da88703f1ba04e354"
dependencies = [
"arrayvec",
"bevy_ecs_macros",
@@ -250,46 +250,46 @@ dependencies = [
[[package]]
name = "bevy_ecs_macro_logic"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee0b98a74141ce8dce4654c60020ebbaefab32646f42af6bbae55f282ae65ff7"
+checksum = "76a10fef45a3e7dad717ba03e8b5f7ecdf3b3fd1cd1c5e425793525938e90d5f"
dependencies = [
"bevy_macro_utils",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
name = "bevy_ecs_macros"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dc958a194d226d618404e0fea99a3c8b4146207a00a3ba07fa712bf71607240"
+checksum = "3916d712264b6f3ec9c964ba10719450ea371e537e202a2ffcfa9f6a5e68169d"
dependencies = [
"bevy_ecs_macro_logic",
"bevy_macro_utils",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
name = "bevy_macro_utils"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "746a19912c6dc1bbe79188778573e8a253d5832c696b2fcb95578c17b29ff7ba"
+checksum = "7164fe229422295bba15f1885048f34f4660db069aacb895208f996b0c7784fb"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
"toml_edit",
]
[[package]]
name = "bevy_platform"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3120670bb2308980f723477c9d82476fcda31f08be9b256c3f0acdee82a65094"
+checksum = "fd610a417e7b4a3b9602cf11264d16cbbff8eabe689c5fed8be4e84c6a2680a3"
dependencies = [
"foldhash",
"futures-channel",
@@ -303,15 +303,15 @@ dependencies = [
[[package]]
name = "bevy_ptr"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b511e89f7078fd21fdea77061a0ca3e737297c7011bb10c073e0aecb17c9fea6"
+checksum = "1974de4b140f102fded747229ff4a61921acec07f02f0d1dcdadfe9ce177d0ff"
[[package]]
name = "bevy_tasks"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa34e6b9b804851ec08a41c0346c859d82e2fa98c906d74b965ee61bbbb688e4"
+checksum = "1f787d4ba7237b64b30bd2d690a8cbae1ba775e0eec326711c446654cb720e7c"
dependencies = [
"async-task",
"atomic-waker",
@@ -325,9 +325,9 @@ dependencies = [
[[package]]
name = "bevy_utils"
-version = "0.19.0"
+version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3aceea6c7ffdd568f866d5ca4dfe50c33440c54f7bc230c13a9ba7ab0c91aed1"
+checksum = "de51fa7ea2e95d4c2d1278c5629285d59eec951cc60c992964c03f30910ef5a1"
dependencies = [
"bevy_platform",
"disqualified",
@@ -348,9 +348,9 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "blocking"
-version = "1.6.2"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
dependencies = [
"async-channel",
"async-task",
@@ -393,15 +393,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
-version = "0.2.1"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "clap"
-version = "4.6.2"
+version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [
"clap_builder",
"clap_derive",
@@ -409,9 +409,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.6.2"
+version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [
"anstream",
"anstyle",
@@ -440,14 +440,14 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.6.1"
+version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
@@ -530,44 +530,6 @@ dependencies = [
"winapi",
]
-[[package]]
-name = "ctl"
-version = "0.2.1"
-dependencies = [
- "anyhow",
- "clap",
- "clap_complete",
- "clap_complete_nushell",
- "comfy-table",
- "hyprwire",
- "hyprwire-protocols-sessiond",
- "hyprwire-scanner",
- "owo-colors",
- "serde_json",
- "termtree",
-]
-
-[[package]]
-name = "daemon"
-version = "0.2.1"
-dependencies = [
- "anyhow",
- "bevy_ecs",
- "calloop",
- "clap",
- "env_logger",
- "hyprwire",
- "hyprwire-protocols-sessiond",
- "hyprwire-scanner",
- "log",
- "rustix",
- "serde",
- "syslog",
- "toml",
- "uzers",
- "zbus",
-]
-
[[package]]
name = "defmt"
version = "1.1.1"
@@ -587,7 +549,7 @@ dependencies = [
"defmt-parser",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -624,7 +586,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
- "syn",
+ "syn 2.0.119",
"unicode-xid",
]
@@ -667,7 +629,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -711,11 +673,10 @@ dependencies = [
[[package]]
name = "event-listener"
-version = "5.4.1"
+version = "5.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
dependencies = [
- "concurrent-queue",
"parking",
"pin-project-lite",
]
@@ -732,9 +693,9 @@ dependencies = [
[[package]]
name = "fastrand"
-version = "2.4.1"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "fixedbitset"
@@ -750,24 +711,24 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "futures-channel"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-io"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-lite"
@@ -784,15 +745,15 @@ dependencies = [
[[package]]
name = "futures-task"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
@@ -877,9 +838,9 @@ dependencies = [
[[package]]
name = "hyprwire"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b50723c9685352232486a20c8435544833926a2119a142868cf478f2642f266"
+checksum = "19ee2af8d569f2a6d71130b9643dc534b7255b57f7953260d044d88d9735e707"
dependencies = [
"hyprwire-core",
"hyprwire-scanner",
@@ -890,38 +851,37 @@ dependencies = [
[[package]]
name = "hyprwire-core"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbed3858de0e63414aa82ba0049672b7f3edf23b40e9fa497929d8f4c749d62b"
+checksum = "1a76bc1faef6e638086f116cdfc19e590232d7565f569e01fd9eb217ac86f35f"
[[package]]
name = "hyprwire-protocols-sessiond"
-version = "0.1.1"
+version = "0.2.0"
dependencies = [
- "bevy_ecs",
"hyprwire",
"hyprwire-scanner",
]
[[package]]
name = "hyprwire-scanner"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "726d74a6c38d8ee1ca2fee6f113b18daf93e92548ea0576a890eb700b2c84698"
+checksum = "2b50d1dc744b592f4354885b2c445f7ba4f61e3472d02e23d7b81d73f39dbf15"
dependencies = [
"hyprwire-core",
"prettyplease",
"proc-macro2",
"quick-xml",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
name = "indexmap"
-version = "2.14.0"
+version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
@@ -941,11 +901,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
-version = "0.2.32"
+version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e"
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
dependencies = [
"defmt",
+ "jiff-core",
"jiff-static",
"log",
"portable-atomic",
@@ -953,37 +914,48 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
[[package]]
name = "jiff-static"
-version = "0.2.32"
+version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc"
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
dependencies = [
+ "jiff-core",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
name = "js-sys"
-version = "0.3.103"
+version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [
"cfg-if",
"futures-util",
+ "once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
-version = "0.2.186"
+version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libsession"
-version = "0.2.1"
+version = "0.0.0"
dependencies = [
"hyprwire",
"hyprwire-protocols-sessiond",
@@ -1027,9 +999,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.33"
+version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
@@ -1109,7 +1081,7 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
[[package]]
name = "pam"
-version = "0.2.1"
+version = "0.0.0"
dependencies = [
"hyprwire",
"hyprwire-protocols-sessiond",
@@ -1194,15 +1166,15 @@ dependencies = [
[[package]]
name = "portable-atomic"
-version = "1.13.1"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
-version = "0.2.7"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
dependencies = [
"portable-atomic",
]
@@ -1220,7 +1192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -1234,9 +1206,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.106"
+version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
@@ -1252,9 +1224,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.46"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -1288,9 +1260,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.16"
+version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
@@ -1345,9 +1317,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
@@ -1355,29 +1327,29 @@ dependencies = [
[[package]]
name = "serde_core"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
name = "serde_json"
-version = "1.0.150"
+version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
@@ -1388,13 +1360,13 @@ dependencies = [
[[package]]
name = "serde_repr"
-version = "0.1.20"
+version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
@@ -1406,6 +1378,44 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "sessionctl"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "clap_complete",
+ "clap_complete_nushell",
+ "comfy-table",
+ "hyprwire",
+ "hyprwire-protocols-sessiond",
+ "hyprwire-scanner",
+ "owo-colors",
+ "serde_json",
+ "termtree",
+]
+
+[[package]]
+name = "sessiond"
+version = "0.2.3"
+dependencies = [
+ "anyhow",
+ "bevy_ecs",
+ "calloop",
+ "clap",
+ "env_logger",
+ "hyprwire",
+ "hyprwire-protocols-sessiond",
+ "hyprwire-scanner",
+ "log",
+ "rustix",
+ "serde",
+ "syslog",
+ "toml",
+ "uzers",
+ "zbus",
+]
+
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@@ -1433,9 +1443,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.15.2"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
[[package]]
name = "spin"
@@ -1469,6 +1479,17 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "syn"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
[[package]]
name = "syslog"
version = "7.0.0"
@@ -1502,29 +1523,29 @@ checksum = "d4d1330fe7f7f872cd05165130b10602d667b205fd85be09be2814b115d4ced9"
[[package]]
name = "thiserror"
-version = "2.0.18"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "2.0.18"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
name = "time"
-version = "0.3.54"
+version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"libc",
@@ -1554,9 +1575,9 @@ dependencies = [
[[package]]
name = "toml"
-version = "1.1.3+spec-1.1.0"
+version = "1.1.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
+checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785"
dependencies = [
"indexmap",
"serde_core",
@@ -1590,9 +1611,9 @@ dependencies = [
[[package]]
name = "toml_parser"
-version = "1.1.2+spec-1.1.0"
+version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
@@ -1623,7 +1644,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -1678,9 +1699,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.24.0"
+version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
+checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"js-sys",
"serde_core",
@@ -1705,7 +1726,7 @@ checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -1716,9 +1737,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasm-bindgen"
-version = "0.2.126"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [
"cfg-if",
"once_cell",
@@ -1729,9 +1750,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.126"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1739,22 +1760,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.126"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.126"
+version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [
"unicode-ident",
]
@@ -1819,9 +1840,9 @@ dependencies = [
[[package]]
name = "zbus"
-version = "5.17.0"
+version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
dependencies = [
"async-broadcast",
"async-executor",
@@ -1854,14 +1875,14 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "5.17.0"
+version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -1869,15 +1890,24 @@ dependencies = [
[[package]]
name = "zbus_names"
-version = "4.3.3"
+version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
dependencies = [
"serde",
"winnow",
"zvariant",
]
+[[package]]
+name = "zcheapstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "zmij"
version = "1.0.23"
@@ -1886,40 +1916,41 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zvariant"
-version = "5.13.0"
+version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7"
+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow",
+ "zcheapstr",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
-version = "5.13.0"
+version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31"
+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
-version = "3.5.0"
+version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
dependencies = [
"proc-macro2",
"quote",
"serde",
- "syn",
+ "syn 3.0.5",
"winnow",
]
diff --git a/Cargo.toml b/Cargo.toml
index 62e0112..12a4061 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[workspace]
members = [
- "ctl",
- "daemon",
+ "sessionctl",
+ "sessiond",
"libsession",
"libsession-rs",
"libsession-sys",
@@ -27,7 +27,6 @@ categories = []
edition = "2024"
license = "GPL-3.0-only"
repository = "https://tangled.org/r0chd.pl/sessiond"
-version = "0.2.1"
description = "session management daemon"
readme = "README.md"
diff --git a/daemon/src/components.rs b/daemon/src/components.rs
deleted file mode 100644
index 6e94cbc..0000000
--- a/daemon/src/components.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-#[derive(bevy_ecs::component::Component)]
-pub struct SessionInhibitor(
- pub crate::session_inhibit_v1::server::session_inhibitor::SessionInhibitor,
-);
diff --git a/default.nix b/default.nix
index 0b655d1..3cd1de8 100644
--- a/default.nix
+++ b/default.nix
@@ -22,10 +22,19 @@ let
};
in
rec {
- default = pkgs.callPackage ./nix/package.nix {
+ default = pkgs.callPackage ./nix/sessiond/package.nix {
inherit rustPlatform;
};
sessiond = default;
+ sessiond-backlight = pkgs.callPackage ./nix/sessiond-backlight/package.nix {
+ inherit rustPlatform;
+ };
+ sessiond-hooks = pkgs.callPackage ./nix/sessiond-hooks/package.nix {
+ inherit rustPlatform;
+ };
+ sessiond-uaccess = pkgs.callPackage ./nix/sessiond-uaccess/package.nix {
+ inherit rustPlatform;
+ };
};
in
{
@@ -37,13 +46,16 @@ in
};
nixosModules = rec {
- default = import ./nix/nixos.nix;
+ default = import ./nix/sessiond/nixos.nix;
sessiond = default;
};
finixModules = rec {
- default = import ./nix/finix.nix;
+ default = import ./nix/sessiond/finix.nix;
sessiond = default;
+ sessiond-backlight = import ./nix/sessiond-backlight/finix.nix;
+ sessiond-hooks = import ./nix/sessiond-hooks/finix.nix;
+ sessiond-uaccess = import ./nix/sessiond-uaccess/finix.nix;
};
formatter = import ./nix/internal/formatter.nix { inherit pkgs rustToolchain; };
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index fcca8fc..fb9eabf 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -102,8 +102,8 @@ cargo build --workspace --release --features consolekit
Build outputs can be found in `target/release`:
-- `daemon`: the session daemon
-- `ctl`: the control client
+- `sessiond`: the session daemon
+- `sessionctl`: the control client
- `libpam.so`: the PAM module
## Other distributions
diff --git a/flake.nix b/flake.nix
index eacd8bd..28f8947 100644
--- a/flake.nix
+++ b/flake.nix
@@ -65,34 +65,48 @@
program = "${script}";
};
- packages = forAllSystems (pkgs: {
- sessiond =
- let
- rustToolchain = pkgs.rust-bin.stable.latest.default.override {
- extensions = [
- "rust-analyzer"
- "rust-src"
- "llvm-tools"
- ];
- };
- rustPlatform = pkgs.makeRustPlatform {
- cargo = rustToolchain;
- rustc = rustToolchain;
- };
- in
- pkgs.callPackage ./nix/package.nix {
+ packages = forAllSystems (
+ pkgs:
+ let
+ rustToolchain = pkgs.rust-bin.stable.latest.default.override {
+ extensions = [
+ "rust-analyzer"
+ "rust-src"
+ "llvm-tools"
+ ];
+ };
+ rustPlatform = pkgs.makeRustPlatform {
+ cargo = rustToolchain;
+ rustc = rustToolchain;
+ };
+ in
+ {
+ sessiond = pkgs.callPackage ./nix/sessiond/package.nix {
inherit rustPlatform;
};
- default = self.packages.${pkgs.stdenv.hostPlatform.system}.sessiond;
- });
+ sessiond-backlight = pkgs.callPackage ./nix/sessiond-backlight/package.nix {
+ inherit rustPlatform;
+ };
+ sessiond-hooks = pkgs.callPackage ./nix/sessiond-hooks/package.nix {
+ inherit rustPlatform;
+ };
+ sessiond-uaccess = pkgs.callPackage ./nix/sessiond-uaccess/package.nix {
+ inherit rustPlatform;
+ };
+ default = self.packages.${pkgs.stdenv.hostPlatform.system}.sessiond;
+ }
+ );
nixosModules = {
- sessiond = import ./nix/nixos.nix;
+ sessiond = import ./nix/sessiond/nixos.nix;
default = self.nixosModules.sessiond;
};
finixModules = {
- sessiond = import ./nix/finix.nix;
+ sessiond = import ./nix/sessiond/finix.nix;
+ sessiond-backlight = import ./nix/sessiond-backlight/finix.nix;
+ sessiond-hooks = import ./nix/sessiond-hooks/finix.nix;
+ sessiond-uaccess = import ./nix/sessiond-uaccess/finix.nix;
default = self.finixModules.sessiond;
};
@@ -117,10 +131,15 @@
nixos-vm = nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
- inherit (self.packages.${system}) sessiond;
+ inherit (self.packages.${system})
+ sessiond
+ sessiond-backlight
+ sessiond-hooks
+ sessiond-uaccess
+ ;
};
modules = [
- ./nix/nixos.nix
+ ./nix/sessiond/nixos.nix
./nix/internal/nixos-vm.nix
];
};
@@ -135,11 +154,19 @@
finix-vm = finix.lib.finixSystem {
inherit (pkgs) lib;
specialArgs = {
- inherit (self.packages.${system}) sessiond;
+ inherit (self.packages.${system})
+ sessiond
+ sessiond-backlight
+ sessiond-hooks
+ sessiond-uaccess
+ ;
};
modules = [
{ nixpkgs = { inherit pkgs; }; }
- ./nix/finix.nix
+ ./nix/sessiond/finix.nix
+ ./nix/sessiond-backlight/finix.nix
+ ./nix/sessiond-hooks/finix.nix
+ ./nix/sessiond-uaccess/finix.nix
./nix/internal/finix-vm.nix
"${finix.outPath}/modules/virtualisation/qemu.nix"
]
diff --git a/hyprwire-protocols-sessiond/Cargo.toml b/hyprwire-protocols-sessiond/Cargo.toml
index 34348bf..83b8b8d 100644
--- a/hyprwire-protocols-sessiond/Cargo.toml
+++ b/hyprwire-protocols-sessiond/Cargo.toml
@@ -5,16 +5,12 @@ categories.workspace = true
edition.workspace = true
license = "MIT"
repository.workspace = true
-version = "0.1.1"
+version = "0.2.0"
description = "Common Hyprwire protocols for sessiond"
readme = "README.md"
[dependencies]
hyprwire = "0.5.1"
-bevy_ecs = { version = "0.19.0", default-features = false, optional = true }
-
-[features]
-bevy_ecs = ["dep:bevy_ecs"]
[build-dependencies]
hyprwire-scanner = "0.5.1"
diff --git a/hyprwire-protocols-sessiond/build.rs b/hyprwire-protocols-sessiond/build.rs
index 996f170..3f44bf1 100644
--- a/hyprwire-protocols-sessiond/build.rs
+++ b/hyprwire-protocols-sessiond/build.rs
@@ -3,15 +3,11 @@ use std::io;
fn main() -> io::Result<()> {
let scanner = hyprwire_scanner::configure()
.with_targets(hyprwire_scanner::Targets::CLIENT | hyprwire_scanner::Targets::SERVER);
- scanner
- .type_attribute(
- ".",
- "#[cfg_attr(feature = \"bevy_ecs\", derive(bevy_ecs::component::Component))]",
- )
- .compile(&[
- "protocols/session_core_v1.xml",
- "protocols/session_inhibit_v1.xml",
- "protocols/session_management_v1.xml",
- "protocols/session_management_v2.xml",
- ])
+ scanner.compile(&[
+ "protocols/session_core_v1.xml",
+ "protocols/session_inhibit_v1.xml",
+ "protocols/session_management_v1.xml",
+ "protocols/session_management_v2.xml",
+ "protocols/session_backlight_v1.xml",
+ ])
}
diff --git a/hyprwire-protocols-sessiond/protocols/session_backlight_v1.xml b/hyprwire-protocols-sessiond/protocols/session_backlight_v1.xml
new file mode 100644
index 0000000..5a1033b
--- /dev/null
+++ b/hyprwire-protocols-sessiond/protocols/session_backlight_v1.xml
@@ -0,0 +1,81 @@
+
+
+
+ Copyright (c) 2026 Oskar Rochowiak
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hyprwire-protocols-sessiond/src/lib.rs b/hyprwire-protocols-sessiond/src/lib.rs
index 95a2d98..5f01206 100644
--- a/hyprwire-protocols-sessiond/src/lib.rs
+++ b/hyprwire-protocols-sessiond/src/lib.rs
@@ -31,18 +31,15 @@ define_protocol_modules!(server => {
v1 => session_management_v1 => "session_management_v1",
v2 => session_management_v2 => "session_management_v2",
},
+ session_backlight => { v1 => session_backlight_v1 => "session_backlight_v1" },
});
define_protocol_modules!(client => {
session_core => { v1 => session_core_v1 => "session_core_v1" },
session_inhibit => { v1 => session_inhibit_v1 => "session_inhibit_v1" },
- session_management => { v1 => session_management_v1 => "session_management_v1" },
+ session_management => {
+ v1 => session_management_v1 => "session_management_v1",
+ v2 => session_management_v2 => "session_management_v2",
+ },
+ session_backlight => { v1 => session_backlight_v1 => "session_backlight_v1" },
});
-
-#[cfg(feature = "bevy_ecs")]
-impl bevy_ecs::component::Component
- for server::session_inhibit::v1::session_inhibit_v1::session_inhibitor::SessionInhibitor
-{
- const STORAGE_TYPE: bevy_ecs::component::StorageType = bevy_ecs::component::StorageType::Table;
- type Mutability = bevy_ecs::component::Mutable;
-}
diff --git a/libsession/Cargo.toml b/libsession/Cargo.toml
index f42326e..68aa08b 100644
--- a/libsession/Cargo.toml
+++ b/libsession/Cargo.toml
@@ -5,7 +5,8 @@ categories.workspace = true
edition.workspace = true
license = "MIT"
repository.workspace = true
-version.workspace = true
+version = "0.0.0"
+publish = false
description.workspace = true
readme.workspace = true
@@ -16,7 +17,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
hyprwire = { version = "0.5.1", optional = true }
zbus = { version = "5.16.0", optional = true }
-hyprwire-protocols-sessiond = { path = "../hyprwire-protocols-sessiond", version = "0.1.1", optional = true }
+hyprwire-protocols-sessiond = { path = "../hyprwire-protocols-sessiond", version = "0.2.0", optional = true }
[build-dependencies]
diff --git a/libsession/libsession.pc.in b/libsession/libsession.pc.in
new file mode 100644
index 0000000..6a8ad70
--- /dev/null
+++ b/libsession/libsession.pc.in
@@ -0,0 +1,10 @@
+prefix=@PREFIX@
+includedir=@INCLUDE@
+libdir=@LIBDIR@
+
+Name: libsession
+URL: https://tangled.org/r0chd.pl/sessiond
+Description: Session management client library
+Version: @LIBSESSION_VERSION@
+Cflags: -I${includedir}
+Libs: -L${libdir} -lsession
diff --git a/nix/internal/finix-checks.nix b/nix/internal/finix-checks.nix
index 0dbccd3..f57cf09 100644
--- a/nix/internal/finix-checks.nix
+++ b/nix/internal/finix-checks.nix
@@ -14,7 +14,7 @@ let
testArgs = {
inherit sessiondTest;
- sessiondModule = import (self + "/nix/finix.nix");
+ sessiondModule = import (self + "/nix/sessiond/finix.nix");
sessiondPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.sessiond;
};
diff --git a/nix/internal/finix-vm.nix b/nix/internal/finix-vm.nix
index a3ef6a0..f48f735 100644
--- a/nix/internal/finix-vm.nix
+++ b/nix/internal/finix-vm.nix
@@ -3,6 +3,9 @@
pkgs,
config,
sessiond,
+ sessiond-hooks,
+ sessiond-uaccess,
+ sessiond-backlight,
...
}:
let
@@ -55,11 +58,33 @@ in
debug = true;
settings = {
users.user_3.linger = true;
- power = {
- suspend = [ "${pkgs.zzz}/bin/zzz" ];
- hibernate = [ "${pkgs.zzz}/bin/ZZZ" ];
- reboot = [ "${pkgs.finit}/bin/reboot" ];
- poweroff = [ "${pkgs.finit}/bin/poweroff" ];
+ };
+ };
+ sessiond-uaccess = {
+ enable = true;
+ package = sessiond-uaccess;
+ logLevel = "debug";
+ };
+ sessiond-backlight = {
+ enable = true;
+ package = sessiond-backlight;
+ logLevel = "debug";
+ };
+ sessiond-hooks = {
+ package = sessiond-hooks;
+ logLevel = "debug";
+ hooks.dinit.settings = {
+ on_start = {
+ exec = lib.getExe' pkgs.dinit "dinit";
+ args = [
+ "--user"
+ "--services-dir"
+ "~/.config/dinit.d"
+ ];
+ };
+ restart = {
+ on = "Failure";
+ delay = 5;
};
};
};
diff --git a/nix/internal/nixos-checks.nix b/nix/internal/nixos-checks.nix
index aa0ff10..4558f40 100644
--- a/nix/internal/nixos-checks.nix
+++ b/nix/internal/nixos-checks.nix
@@ -10,7 +10,7 @@ let
testArgs = {
inherit sessiondTest;
- sessiondModule = import (self + "/nix/nixos.nix");
+ sessiondModule = import (self + "/nix/sessiond/nixos.nix");
sessiondPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.sessiond;
};
diff --git a/nix/internal/nixos-vm.nix b/nix/internal/nixos-vm.nix
index 0f3bca5..bd982c6 100644
--- a/nix/internal/nixos-vm.nix
+++ b/nix/internal/nixos-vm.nix
@@ -3,6 +3,9 @@
pkgs,
modulesPath,
sessiond,
+ sessiond-backlight,
+ sessiond-hooks,
+ sessiond-uaccess,
...
}:
let
@@ -18,6 +21,13 @@ in
package = sessiond;
};
+ environment.systemPackages = [
+ sessiond
+ sessiond-backlight
+ sessiond-hooks
+ sessiond-uaccess
+ ];
+
boot.kernelParams = [
"console=ttyS0"
];
diff --git a/nix/internal/shell.nix b/nix/internal/shell.nix
index 02584cd..fd47b50 100644
--- a/nix/internal/shell.nix
+++ b/nix/internal/shell.nix
@@ -6,11 +6,6 @@
pkgs.mkShell {
name = "sessiond-devshell";
packages = builtins.attrValues {
- sessiond-testctl = pkgs.writeShellScriptBin "sessiond-testctl" ''
- printf 'sessiond-testctl args:'
- printf ' <%s>' "$@"
- printf '\n'
- '';
inherit
rustToolchain
;
@@ -20,6 +15,8 @@ pkgs.mkShell {
npins
linux-pam
pkg-config
+ udev
+ acl
;
};
@@ -27,11 +24,4 @@ pkgs.mkShell {
buildInputs = [ sessiond ];
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
-
- shellHook = ''
- if [[ $- == *i* ]] && [ -z "''${IN_NIX_SHELL_NU:-}" ]; then
- export IN_NIX_SHELL_NU=1
- exec ${pkgs.nushell}/bin/nu
- fi
- '';
}
diff --git a/nix/sessiond-backlight/finix.nix b/nix/sessiond-backlight/finix.nix
new file mode 100644
index 0000000..c2c4ca2
--- /dev/null
+++ b/nix/sessiond-backlight/finix.nix
@@ -0,0 +1,48 @@
+{
+ lib,
+ pkgs,
+ config,
+ ...
+}:
+let
+ inherit (lib) types;
+ cfg = config.services.sessiond-backlight;
+in
+{
+ options.services.sessiond-backlight = {
+ enable = lib.mkEnableOption "sessiond-backlight";
+ package = lib.mkPackageOption pkgs "sessiond-backlight" { };
+ debug = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable debug logging.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ services.dbus.packages = [ cfg.package ];
+
+ finit.services.sessiond-backlight = {
+ description = "";
+ conditions = [
+ "service/sessiond/ready"
+ ];
+ command = lib.escapeShellArgs [
+ (lib.getExe cfg.package)
+ "--log-target"
+ "syslog"
+ ];
+ environment =
+ if cfg.debug then
+ {
+ LOG_LEVEL = "debug";
+ }
+ else
+ {
+ LOG_LEVEL = lib.mkDefault "info";
+ };
+ };
+ };
+}
diff --git a/nix/sessiond-backlight/package.nix b/nix/sessiond-backlight/package.nix
new file mode 100644
index 0000000..5cdb600
--- /dev/null
+++ b/nix/sessiond-backlight/package.nix
@@ -0,0 +1,31 @@
+{
+ lib,
+ rustPlatform,
+}:
+let
+ cargoToml = fromTOML (builtins.readFile ../../sessiond-backlight/Cargo.toml);
+in
+rustPlatform.buildRustPackage {
+ pname = cargoToml.package.name;
+ inherit (cargoToml.package) version;
+
+ cargoLock.lockFile = ../../sessiond-backlight/Cargo.lock;
+
+ buildAndTestSubdir = "sessiond-backlight";
+
+ postUnpack = "cp source/sessiond-backlight/Cargo.lock source/Cargo.lock";
+
+ src = lib.cleanSourceWith {
+ src = ../..;
+ filter = path: type: lib.cleanSourceFilter path type || lib.hasSuffix "/Cargo.lock" (toString path);
+ };
+
+ meta = {
+ description = cargoToml.package.description;
+ homepage = cargoToml.package.repository;
+ license = lib.licenses.gpl3Only;
+ maintainers = builtins.attrValues { inherit (lib.maintainers) r0chd; };
+ platforms = lib.platforms.linux;
+ mainProgram = "sessiond-backlight";
+ };
+}
diff --git a/nix/sessiond-hooks/finix.nix b/nix/sessiond-hooks/finix.nix
new file mode 100644
index 0000000..2f18ee4
--- /dev/null
+++ b/nix/sessiond-hooks/finix.nix
@@ -0,0 +1,73 @@
+{
+ lib,
+ pkgs,
+ config,
+ ...
+}:
+let
+ inherit (lib) types;
+ cfg = config.services.sessiond-hooks;
+ tomlFormat = pkgs.formats.toml { };
+in
+{
+ options.services.sessiond-hooks = {
+ package = lib.mkPackageOption pkgs "sessiond-hooks" { };
+ debug = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable debug logging.
+ '';
+ };
+ hooks = lib.mkOption {
+ type = types.attrsOf (
+ types.submodule {
+ options = {
+ enable = lib.mkOption {
+ type = types.bool;
+ default = true;
+ };
+ settings = lib.mkOption {
+ type = tomlFormat.type;
+ description = "Hook settings serialized to TOML.";
+ };
+ };
+ }
+ );
+ default = { };
+ description = ''
+ Per-user session hooks launched. Each named entry spawns an
+ independent `sessiond-hooks` process. '';
+ };
+ };
+
+ config.finit.services = lib.mapAttrs' (
+ name: hook:
+ let
+ configFile = tomlFormat.generate "sessiond-hooks-${name}.toml" hook.settings;
+ in
+ lib.nameValuePair "sessiond-hooks-${name}" {
+ inherit (hook) enable;
+ description = "sessiond per-user hook: ${name}";
+ conditions = [
+ "service/sessiond/ready"
+ ];
+ command = lib.escapeShellArgs [
+ (lib.getExe cfg.package)
+ "--log-target"
+ "syslog"
+ "--config"
+ "${configFile}"
+ ];
+ environment =
+ if cfg.debug then
+ {
+ LOG_LEVEL = "debug";
+ }
+ else
+ {
+ LOG_LEVEL = lib.mkDefault "info";
+ };
+ }
+ ) cfg.hooks;
+}
diff --git a/nix/sessiond-hooks/package.nix b/nix/sessiond-hooks/package.nix
new file mode 100644
index 0000000..586dc30
--- /dev/null
+++ b/nix/sessiond-hooks/package.nix
@@ -0,0 +1,31 @@
+{
+ lib,
+ rustPlatform,
+}:
+let
+ cargoToml = fromTOML (builtins.readFile ../../sessiond-hooks/Cargo.toml);
+in
+rustPlatform.buildRustPackage {
+ pname = cargoToml.package.name;
+ inherit (cargoToml.package) version;
+
+ cargoLock.lockFile = ../../sessiond-hooks/Cargo.lock;
+
+ buildAndTestSubdir = "sessiond-hooks";
+
+ postUnpack = "cp source/sessiond-hooks/Cargo.lock source/Cargo.lock";
+
+ src = lib.cleanSourceWith {
+ src = ../..;
+ filter = path: type: lib.cleanSourceFilter path type || lib.hasSuffix "/Cargo.lock" (toString path);
+ };
+
+ meta = {
+ description = cargoToml.package.description;
+ homepage = cargoToml.package.repository;
+ license = lib.licenses.gpl3Only;
+ maintainers = builtins.attrValues { inherit (lib.maintainers) r0chd; };
+ platforms = lib.platforms.linux;
+ mainProgram = "sessiond-hooks";
+ };
+}
diff --git a/nix/sessiond-uaccess/finix.nix b/nix/sessiond-uaccess/finix.nix
new file mode 100644
index 0000000..bb40cf9
--- /dev/null
+++ b/nix/sessiond-uaccess/finix.nix
@@ -0,0 +1,89 @@
+{
+ lib,
+ pkgs,
+ config,
+ ...
+}:
+let
+ inherit (lib) types;
+ cfg = config.services.sessiond-uaccess;
+ extraRuleNames = builtins.attrNames cfg.extraRules;
+ validRuleName = name: builtins.match "[0-9A-Za-z._+-]+[.]lua" name != null;
+ extraRulesPackage = pkgs.linkFarm "sessiond-uaccess-extra-rules" (
+ lib.mapAttrsToList (name: text: {
+ inherit name;
+ path = pkgs.writeText name text;
+ }) cfg.extraRules
+ );
+in
+{
+ options.services.sessiond-uaccess = {
+ enable = lib.mkEnableOption "sessiond-uaccess";
+ package = lib.mkPackageOption pkgs "sessiond-uaccess" { };
+ debug = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable debug logging.
+ '';
+ };
+ extraRules = lib.mkOption {
+ type = types.attrsOf types.lines;
+ default = { };
+ example = {
+ "90-local.lua" = ''
+ device {
+ access = "rw",
+ match = function(dev)
+ return dev.subsystem == "backlight"
+ end,
+ }
+ '';
+ };
+ description = ''
+ Additional Lua rule files loaded after the packaged sessiond-uaccess
+ rules. Attribute names are used as filenames and should end in
+ {file}`.lua`; numeric prefixes can be used to express ordering.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = builtins.all validRuleName extraRuleNames;
+ message = "services.sessiond-uaccess.extraRules attribute names must be simple Lua filenames ending in .lua.";
+ }
+ ];
+
+ finit.services.sessiond-uaccess = {
+ description = "Grant device access to active local sessions";
+ conditions = [
+ "service/sessiond/ready"
+ "service/syslogd/ready"
+ ];
+ command = lib.escapeShellArgs (
+ [
+ (lib.getExe cfg.package)
+ "--log-target"
+ "syslog"
+ "--rules-dirs"
+ "${cfg.package}/share/sessiond-uaccess/rules"
+ ]
+ ++ lib.optionals (cfg.extraRules != { }) [
+ "--rules-dirs"
+ "${extraRulesPackage}"
+ ]
+ );
+ environment =
+ if cfg.debug then
+ {
+ LOG_LEVEL = "debug";
+ }
+ else
+ {
+ LOG_LEVEL = lib.mkDefault "info";
+ };
+ };
+ };
+}
diff --git a/nix/sessiond-uaccess/package.nix b/nix/sessiond-uaccess/package.nix
new file mode 100644
index 0000000..746b572
--- /dev/null
+++ b/nix/sessiond-uaccess/package.nix
@@ -0,0 +1,45 @@
+{
+ lib,
+ rustPlatform,
+ pkg-config,
+ udev,
+ acl,
+}:
+let
+ cargoToml = fromTOML (builtins.readFile ../../sessiond-uaccess/Cargo.toml);
+in
+rustPlatform.buildRustPackage (finalAttrs: {
+ pname = cargoToml.package.name;
+ inherit (cargoToml.package) version;
+
+ cargoLock.lockFile = ../../sessiond-uaccess/Cargo.lock;
+
+ buildAndTestSubdir = "sessiond-uaccess";
+
+ postUnpack = "cp source/sessiond-uaccess/Cargo.lock source/Cargo.lock";
+
+ src = lib.cleanSourceWith {
+ src = ../..;
+ filter = path: type: lib.cleanSourceFilter path type || lib.hasSuffix "/Cargo.lock" (toString path);
+ };
+
+ nativeBuildInputs = [ pkg-config ];
+ buildInputs = [
+ acl
+ udev
+ ];
+
+ postInstall = ''
+ install -d $out/share/sessiond-uaccess
+ cp -r ${finalAttrs.src}/sessiond-uaccess/rules $out/share/sessiond-uaccess/
+ '';
+
+ meta = {
+ description = cargoToml.package.description;
+ homepage = cargoToml.package.repository;
+ license = lib.licenses.gpl3Only;
+ maintainers = builtins.attrValues { inherit (lib.maintainers) r0chd; };
+ platforms = lib.platforms.linux;
+ mainProgram = "sessiond-uaccess";
+ };
+})
diff --git a/nix/common.nix b/nix/sessiond/common.nix
similarity index 90%
rename from nix/common.nix
rename to nix/sessiond/common.nix
index 3b40f28..dcf5480 100644
--- a/nix/common.nix
+++ b/nix/sessiond/common.nix
@@ -5,11 +5,12 @@
}:
let
format = pkgs.formats.toml { };
+ inherit (lib) types;
in
{
options.services.sessiond = {
enable = lib.mkOption {
- type = lib.types.bool;
+ type = types.bool;
default = false;
description = ''
Whether to enable [sessiond](${pkgs.sessiond.meta.homepage}) as a system service.
@@ -17,7 +18,7 @@ in
};
package = lib.mkOption {
- type = lib.types.package;
+ type = types.package;
default = pkgs.sessiond;
defaultText = lib.literalExpression "pkgs.sessiond";
description = ''
@@ -26,7 +27,7 @@ in
};
debug = lib.mkOption {
- type = lib.types.bool;
+ type = types.bool;
default = false;
description = ''
Whether to enable debug logging.
diff --git a/nix/finix.nix b/nix/sessiond/finix.nix
similarity index 100%
rename from nix/finix.nix
rename to nix/sessiond/finix.nix
diff --git a/nix/nixos.nix b/nix/sessiond/nixos.nix
similarity index 86%
rename from nix/nixos.nix
rename to nix/sessiond/nixos.nix
index ee27b80..da20a7e 100644
--- a/nix/nixos.nix
+++ b/nix/sessiond/nixos.nix
@@ -7,7 +7,7 @@
let
cfg = config.services.sessiond;
settingsFormat = pkgs.formats.toml { };
- daemonConfig = settingsFormat.generate "sessiond.toml" cfg.settings;
+ sessiondConfig = settingsFormat.generate "sessiond.toml" cfg.settings;
in
{
imports = [ ./common.nix ];
@@ -21,7 +21,7 @@ in
wantedBy = [ "default.target" ];
serviceConfig = {
- ExecStart = "${cfg.package}/bin/sessiond --config ${daemonConfig}";
+ ExecStart = "${cfg.package}/bin/sessiond --config ${sessiondConfig}";
Environment = if cfg.debug then "LOG_LEVEL=debug" else "LOG_LEVEL=info";
Type = "notify";
Delegate = true;
diff --git a/nix/package.nix b/nix/sessiond/package.nix
similarity index 64%
rename from nix/package.nix
rename to nix/sessiond/package.nix
index d9de760..6261a3e 100644
--- a/nix/package.nix
+++ b/nix/sessiond/package.nix
@@ -13,21 +13,11 @@
installShellFiles,
}:
let
- cargoToml = fromTOML (builtins.readFile ../Cargo.toml);
-
- # libsession-sys/libsession-rs link against libsession, which
- # only exists once this derivation's own postInstall runs
- cargoWorkspaceFlags = [
- "--workspace"
- "--exclude"
- "libsession-sys"
- "--exclude"
- "libsession-rs"
- ];
+ cargoToml = fromTOML (builtins.readFile ../../sessiond/Cargo.toml);
in
-rustPlatform.buildRustPackage {
+rustPlatform.buildRustPackage (finalAttrs: {
pname = "sessiond";
- inherit (cargoToml.workspace.package) version;
+ inherit (cargoToml.package) version;
outputs = [
"out"
@@ -36,23 +26,23 @@ rustPlatform.buildRustPackage {
];
cargoLock = {
- lockFile = ../Cargo.lock;
+ lockFile = ../../Cargo.lock;
outputHashes = {
"pamsm-0.5.5" = "sha256-wmSKEVSgd36kwa9ky4H9+ubnqUUwyUPw/OjK8v3wGbk=";
};
};
src = lib.cleanSourceWith {
- src = ../.;
+ src = ../..;
filter =
path: type:
let
- relPath = lib.removePrefix (toString ../. + "/") (toString path);
+ relPath = lib.removePrefix (toString ../.. + "/") (toString path);
in
lib.any (p: lib.hasPrefix p relPath) [
- "ctl"
+ "sessionctl"
"pam"
- "daemon"
+ "sessiond"
"libsession"
"libsession-sys"
"libsession-rs"
@@ -73,37 +63,39 @@ rustPlatform.buildRustPackage {
linux-pam
];
- cargoBuildFlags = cargoWorkspaceFlags;
- cargoTestFlags = cargoWorkspaceFlags;
+ cargoBuildFlags = [
+ "-p"
+ "sessiond"
+ "-p"
+ "sessionctl"
+ "-p"
+ "pam"
+ "-p"
+ "libsession"
+ "-p"
+ "hyprwire-protocols-sessiond"
+ ];
buildNoDefaultFeatures = true;
+ doCheck = false;
buildFeatures =
lib.optionals withConsoleKit [
- "daemon/consolekit"
+ "sessiond/consolekit"
]
++ map (backend: "libsession/${backend}") libsessionBackends;
postInstall = ''
mkdir -p $out/lib/security
mv $out/lib/libpam.so $out/lib/security/pam_sessiond.so
- mv $out/bin/daemon $out/bin/sessiond
- mv $out/bin/ctl $out/bin/sessionctl
-
install -Dm644 libsession/include/libsession.h $out/include/libsession.h
mkdir -p $out/lib/pkgconfig
- cat > $out/lib/pkgconfig/libsession.pc < Result<(), BrightnessError> {
+ let mut components = path::Path::new(value).components();
+ if value.is_empty()
+ || !matches!(components.next(), Some(path::Component::Normal(_)))
+ || components.next().is_some()
+ {
+ return Err(BrightnessError::InvalidArgs(format!(
+ "invalid brightness {kind} {value}"
+ )));
+ }
+
+ Ok(())
+ }
+
+ fn open(
+ session_backlight_device: session_backlight_device::SessionBacklightDevice,
+ subsystem: String,
+ name: String,
+ ) -> Result {
+ if !matches!(subsystem.as_str(), "backlight" | "leds") {
+ return Err(BrightnessError::InvalidArgs(format!(
+ "unsupported brightness subsystem {subsystem}"
+ )));
+ }
+ Self::validate_component("device name", &name)?;
+
+ let path = path::Path::new("/sys/class").join(&subsystem).join(&name);
+
+ let max_brightness = fs::read_to_string(path.join("max_brightness"))
+ .map_err(|err| {
+ BrightnessError::Failed(format!(
+ "failed to read {subsystem}/{name} max_brightness: {err}"
+ ))
+ })?
+ .trim()
+ .parse::()
+ .map_err(|err| {
+ BrightnessError::Failed(format!(
+ "failed to parse {subsystem}/{name} max_brightness: {err}"
+ ))
+ })?;
+
+ let brightness = fs::OpenOptions::new()
+ .write(true)
+ .open(path.join("brightness"))
+ .map_err(|err| {
+ BrightnessError::Failed(format!(
+ "failed to open {subsystem}/{name} brightness: {err}"
+ ))
+ })?;
+
+ log::debug!("opened {subsystem}/{name} with max_brightness={max_brightness}");
+
+ Ok(Self {
+ subsystem,
+ name,
+ brightness,
+ max_brightness,
+ object: session_backlight_device,
+ })
+ }
+
+ fn set(&mut self, brightness: u32) -> anyhow::Result<()> {
+ if brightness > self.max_brightness {
+ anyhow::bail!(
+ "brightness {brightness} exceeds max_brightness {}",
+ self.max_brightness
+ );
+ }
+
+ self.brightness.seek(io::SeekFrom::Start(0))?;
+ self.brightness
+ .write_all(brightness.to_string().as_bytes())?;
+ self.brightness.write_all(b"\n")?;
+ self.brightness.flush()?;
+
+ Ok(())
+ }
+}
+
+struct Session {
+ id: u32,
+ object: session_core_session::SessionCoreSession,
+ local: bool,
+ uid: u32,
+ active: session_core_v1::ActiveState,
+ done: bool,
+}
+
+impl Session {
+ const fn new(id: u32, object: session_core_session::SessionCoreSession) -> Self {
+ Self {
+ id,
+ object,
+ local: false,
+ uid: 0,
+ done: false,
+ active: session_core_v1::ActiveState::Opening,
+ }
+ }
+}
+
+#[derive(Clone, Copy)]
+struct SessionAccess {
+ id: u32,
+ local: bool,
+ uid: u32,
+ active: session_core_v1::ActiveState,
+ done: bool,
+}
+
+impl From<&Session> for SessionAccess {
+ fn from(session: &Session) -> Self {
+ Self {
+ id: session.id,
+ local: session.local,
+ uid: session.uid,
+ active: session.active,
+ done: session.done,
+ }
+ }
+}
+
+struct SessionAccessState {
+ sessions: Vec,
+}
+
+impl SessionAccessState {
+ const fn new() -> Self {
+ Self {
+ sessions: Vec::new(),
+ }
+ }
+
+ fn update(&mut self, session: &Session) {
+ let access = SessionAccess::from(session);
+
+ if let Some(existing) = self
+ .sessions
+ .iter_mut()
+ .find(|existing| existing.id == session.id)
+ {
+ *existing = access;
+ } else {
+ self.sessions.push(access);
+ }
+ }
+
+ fn remove(&mut self, session_id: u32) {
+ self.sessions.retain(|session| session.id != session_id);
+ }
+
+ fn has_active_local_session(&self, uid: u32) -> bool {
+ self.sessions.iter().any(|session| {
+ session.done
+ && session.local
+ && session.uid == uid
+ && matches!(session.active, session_core_v1::ActiveState::Active)
+ })
+ }
+}
+
+struct SessiondBacklight {
+ session_core_manager: Option,
+ sessions: Vec,
+ access: SessionAccessState,
+ devices: Vec,
+}
+
+impl hyprwire::Dispatch for SessiondBacklight {
+ fn event(
+ &mut self,
+ object: &session_core_manager::SessionCoreManager,
+ event: ::Event<'_>,
+ ) {
+ if let session_core_manager::Event::SessionAdded { session_id } = event {
+ self.sessions.push(Session::new(
+ session_id,
+ object.send_get_session::(session_id).unwrap(),
+ ));
+ }
+ }
+}
+
+impl hyprwire::Dispatch for SessiondBacklight {
+ fn event(
+ &mut self,
+ object: &session_core_session::SessionCoreSession,
+ event: ::Event<'_>,
+ ) {
+ let Some(session) = self
+ .sessions
+ .iter_mut()
+ .find(|session| session.object == *object)
+ else {
+ return;
+ };
+
+ match event {
+ session_core_session::Event::Active { state } => session.active = state,
+ session_core_session::Event::Uid { uid } => session.uid = uid,
+ session_core_session::Event::IsLocal { is_local } => session.local = is_local != 0,
+ session_core_session::Event::Done => session.done = true,
+ session_core_session::Event::Destroyed => {
+ let session_id = session.id;
+ self.sessions.retain(|session| session.id != session_id);
+ self.access.remove(session_id);
+ return;
+ }
+ _ => {}
+ }
+
+ self.access.update(session);
+ }
+}
+
+impl hyprwire::Handler for SessiondBacklight {}
+
+impl hyprwire::Dispatch for SessiondBacklight {
+ fn event(
+ &mut self,
+ object: &session_backlight_manager::SessionBacklightManager,
+ event: ::Event<'_>,
+ ) {
+ match event {
+ session_backlight_manager::Event::Destroy => {}
+ session_backlight_manager::Event::GetDevice {
+ session_backlight_device,
+ subsystem,
+ name,
+ } => match Device::open(session_backlight_device, subsystem, name) {
+ Ok(device) => self.devices.push(device),
+ Err(error) => {
+ let (code, message) = match error {
+ BrightnessError::InvalidArgs(message) => {
+ (session_backlight_v1::ErrorCode::InvalidArgs, message)
+ }
+ BrightnessError::Failed(message) => {
+ (session_backlight_v1::ErrorCode::Io, message)
+ }
+ };
+ object.send_error(code, message);
+ }
+ },
+ _ => {}
+ }
+ }
+}
+
+impl hyprwire::Dispatch for SessiondBacklight {
+ fn event(
+ &mut self,
+ object: &session_backlight_device::SessionBacklightDevice,
+ event: ::Event<'_>,
+ ) {
+ match event {
+ session_backlight_device::Event::SetBrightness { brightness } => {
+ let client = object.client();
+ let creds = client.creds();
+
+ let Some(device) = self
+ .devices
+ .iter_mut()
+ .find(|device| device.object == *object)
+ else {
+ return;
+ };
+
+ log::debug!(
+ "SetBrightness called: uid={}, pid={}, subsystem={}, name={}, brightness={brightness}",
+ creds.uid.as_raw(),
+ creds.pid.as_raw_pid(),
+ device.subsystem,
+ device.name,
+ );
+
+ if !self.access.has_active_local_session(creds.uid.as_raw()) && !creds.uid.is_root()
+ {
+ object.send_error(
+ session_backlight_v1::ErrorCode::PermissionDenied,
+ format!(
+ "caller uid {} has no active local session",
+ creds.uid.as_raw()
+ ),
+ );
+ return;
+ }
+
+ match device.set(brightness) {
+ Ok(()) => object.send_done(),
+ Err(message) => {
+ object.send_error(session_backlight_v1::ErrorCode::Io, message.to_string());
+ }
+ }
+ }
+ session_backlight_device::Event::Destroy => {
+ self.devices.retain(|device| device.object != *object);
+ }
+ _ => {}
+ }
+ }
+}
+
+fn main() -> anyhow::Result<()> {
+ let cli = Cli::parse();
+
+ let log_level = match env::var("LOG_LEVEL") {
+ Ok(log) if log == "trace" => log::LevelFilter::Trace,
+ Ok(log) if log == "debug" => log::LevelFilter::Debug,
+ Ok(log) if log == "warn" => log::LevelFilter::Warn,
+ Ok(log) if log == "error" => log::LevelFilter::Error,
+ Ok(log) if log == "off" => log::LevelFilter::Off,
+ _ => log::LevelFilter::Info,
+ };
+
+ match cli.log_target {
+ LogTarget::Stderr => {
+ env_logger::Builder::new()
+ .filter(Some("daemon"), log_level)
+ .init();
+ }
+ LogTarget::Syslog => {
+ syslog::init(
+ syslog::Facility::LOG_DAEMON,
+ log_level,
+ Some("sessiond-backlight"),
+ )?;
+ }
+ }
+
+ let mut event_loop = calloop::EventLoop::try_new()?;
+
+ let mut sessiond_backlight = SessiondBacklight {
+ session_core_manager: None,
+ sessions: Vec::new(),
+ access: SessionAccessState::new(),
+ devices: Vec::new(),
+ };
+
+ let socket_path = path::Path::new("/run/sessiond-backlight.sock");
+ let mut server = server::Server::bind(&socket_path)?;
+ fs::set_permissions(socket_path, fs::Permissions::from_mode(0o666))?;
+
+ server.add_implementation::(
+ &mut sessiond_backlight,
+ 1,
+ );
+
+ let mut client = client::Client::connect("/run/sessiond/sessiond.sock")?;
+ let eq = client.new_event_queue();
+
+ client.add_implementation::();
+
+ eq.wait_for_handshake(&mut sessiond_backlight)?;
+
+ sessiond_backlight.session_core_manager =
+ Some(client.bind::(
+ &eq,
+ &mut sessiond_backlight,
+ 1,
+ )?);
+
+ let fd_wrapper = unsafe { generic::FdWrapper::new(client.extract_loop_fd().as_raw_fd()) };
+ let source = generic::Generic::new(
+ fd_wrapper,
+ calloop::Interest {
+ readable: true,
+ writable: false,
+ },
+ calloop::Mode::Level,
+ );
+ event_loop
+ .handle()
+ .insert_source(source, move |_, _, state| {
+ _ = eq.dispatch_events(state, false);
+ Ok(calloop::PostAction::Continue)
+ })?;
+
+ let fd_wrapper = unsafe { generic::FdWrapper::new(server.extract_loop_fd().as_raw_fd()) };
+ let source = generic::Generic::new(
+ fd_wrapper,
+ calloop::Interest {
+ readable: true,
+ writable: false,
+ },
+ calloop::Mode::Level,
+ );
+ event_loop
+ .handle()
+ .insert_source(source, move |_, _, state| {
+ _ = server.dispatch_events(state, false);
+ Ok(calloop::PostAction::Continue)
+ })?;
+
+ event_loop.run(None, &mut sessiond_backlight, |_| {})?;
+
+ Ok(())
+}
diff --git a/sessiond-hooks/.gitignore b/sessiond-hooks/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/sessiond-hooks/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/sessiond-hooks/Cargo.lock b/sessiond-hooks/Cargo.lock
new file mode 100644
index 0000000..5a2432f
--- /dev/null
+++ b/sessiond-hooks/Cargo.lock
@@ -0,0 +1,776 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "calloop"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
+dependencies = [
+ "bitflags 2.13.1",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "env_filter"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hostname"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-link",
+]
+
+[[package]]
+name = "hyprwire"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ee2af8d569f2a6d71130b9643dc534b7255b57f7953260d044d88d9735e707"
+dependencies = [
+ "hyprwire-core",
+ "hyprwire-scanner",
+ "polling",
+ "rustix",
+]
+
+[[package]]
+name = "hyprwire-core"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a76bc1faef6e638086f116cdfc19e590232d7565f569e01fd9eb217ac86f35f"
+
+[[package]]
+name = "hyprwire-protocols-sessiond"
+version = "0.2.0"
+dependencies = [
+ "hyprwire",
+ "hyprwire-scanner",
+]
+
+[[package]]
+name = "hyprwire-scanner"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b50d1dc744b592f4354885b2c445f7ba4f61e3472d02e23d7b81d73f39dbf15"
+dependencies = [
+ "hyprwire-core",
+ "prettyplease",
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "jiff"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "sessiond-hooks"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "calloop",
+ "clap",
+ "env_logger",
+ "hyprwire",
+ "hyprwire-protocols-sessiond",
+ "log",
+ "rustix",
+ "serde",
+ "syslog",
+ "toml",
+ "uzers",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syslog"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "019f1500a13379b7d051455df397c75770de6311a7a188a699499502704d9f10"
+dependencies = [
+ "hostname",
+ "libc",
+ "log",
+ "time",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uzers"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8275fb1afee25b4111d2dc8b5c505dbbc4afd0b990cb96deb2d88bff8be18d"
+dependencies = [
+ "libc",
+ "log",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
diff --git a/sessiond-hooks/Cargo.toml b/sessiond-hooks/Cargo.toml
new file mode 100644
index 0000000..8b32c1f
--- /dev/null
+++ b/sessiond-hooks/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "sessiond-hooks"
+keywords = []
+categories = []
+edition = "2024"
+license = "GPL-3.0-only"
+repository = "https://tangled.org/r0chd.pl/sessiond"
+version = "0.0.0"
+publish = false
+description = "Launch and supervise per-user session hooks with sessiond"
+readme = "README.md"
+
+[dependencies]
+anyhow = "1.0.103"
+calloop = "0.14.4"
+clap = { version = "4.5.51", features = ["derive"] }
+env_logger = "0.11.8"
+hyprwire = "0.5.1"
+hyprwire-protocols-sessiond = { path = "../hyprwire-protocols-sessiond", version = "0.2.0" }
+log = { version = "0.4.28", features = ["kv"] }
+rustix = "1.1.4"
+syslog = "7.0.0"
+uzers = "0.12.2"
+serde = { version = "1.0.228", features = ["derive", "rc"] }
+toml = "1.1.2"
+[workspace]
diff --git a/sessiond-hooks/LICENSE b/sessiond-hooks/LICENSE
new file mode 100644
index 0000000..368976c
--- /dev/null
+++ b/sessiond-hooks/LICENSE
@@ -0,0 +1,13 @@
+Copyright (C) 2026 Oskar Rochowiak
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of version 3 of the GNU General Public License as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
diff --git a/sessiond-hooks/README.md b/sessiond-hooks/README.md
new file mode 100644
index 0000000..73cd5bb
--- /dev/null
+++ b/sessiond-hooks/README.md
@@ -0,0 +1,98 @@
+# sessiond-hooks
+
+`sessiond-hooks` is a `sessiond` client that launches and supervises a per-user
+session hook (`dinit --user`, `runsvdir`, …) started for every unique UID.
+
+## Installation
+
+`sessiond-hooks` requires `sessiond` to be installed and enabled first.
+
+On finix with flakes:
+
+```nix
+{
+ inputs.sessiond-hooks.url = "git+https://tangled.org/r0chd.pl/sessiond-hooks";
+}
+```
+
+```nix
+{ inputs, lib, pkgs, ... }:
+{
+ imports = [
+ inputs.sessiond-hooks.finixModules.default
+ ];
+
+ services.sessiond-hooks = {
+ package = inputs.sessiond-hooks.packages.${pkgs.system}.default;
+ hooks.dinit.settings = {
+ on_start = {
+ exec = lib.getExe' pkgs.dinit "dinit";
+ args = [ "--user" "--services-dir" "~/.config/dinit.d" ];
+ };
+ restart = {
+ on = "Failure";
+ delay = 5;
+ };
+ };
+ };
+}
+```
+
+With npins:
+
+```sh
+npins add git --name sessiond-hooks https://tangled.org/r0chd.pl/sessiond-hooks --branch master
+```
+
+```nix
+{ lib, pkgs, ... }:
+let
+ sources = import ./npins;
+ sessiond-hooks = import sources.sessiond-hooks.outPath { };
+in
+{
+ imports = [
+ sessiond-hooks.finixModules.default
+ ];
+
+ services.sessiond-hooks = {
+ package = sessiond-hooks.packages.sessiond-hooks;
+ hooks.dinit.settings = {
+ on_start = {
+ exec = lib.getExe' pkgs.dinit "dinit";
+ args = [ "--user" "--services-dir" "~/.config/dinit.d" ];
+ };
+ restart = {
+ on = "Failure";
+ delay = 5;
+ };
+ };
+ };
+}
+```
+
+To build it manually:
+
+```sh
+cargo build --release
+```
+
+The binary is written to `target/release/sessiond-hooks`.
+
+## Usage
+
+```sh
+sessiond-hooks [--config PATH] [--log-target stderr|syslog]
+```
+
+Example configuration:
+
+```toml
+[on_start]
+exec = "/usr/bin/dinit"
+args = ["--user", "--services-dir", "~/.config/dinit.d"]
+
+[restart]
+on = "Failure"
+delay = 5
+```
diff --git a/sessiond-hooks/src/config.rs b/sessiond-hooks/src/config.rs
new file mode 100644
index 0000000..26c2881
--- /dev/null
+++ b/sessiond-hooks/src/config.rs
@@ -0,0 +1,92 @@
+use anyhow::Context as _;
+use serde::Deserialize;
+use std::{fs, io, path};
+
+#[derive(Clone, Copy, Default, Deserialize)]
+pub enum RestartPolicy {
+ #[default]
+ Failure,
+ Never,
+ Always,
+}
+
+#[derive(Deserialize)]
+pub struct Restart {
+ pub on: RestartPolicy,
+ pub delay: u64,
+}
+
+impl Default for Restart {
+ fn default() -> Self {
+ Self {
+ on: RestartPolicy::default(),
+ delay: 5,
+ }
+ }
+}
+
+#[derive(Deserialize)]
+pub struct Config {
+ pub on_start: Command,
+ pub restart: Restart,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct Command {
+ pub exec: String,
+ pub args: Vec,
+}
+
+impl Command {
+ pub fn expand_home(&self, home: &str) -> Self {
+ fn expand(value: &str, home: &str) -> String {
+ if value == "~" {
+ return home.to_owned();
+ }
+
+ match value.strip_prefix("~/") {
+ Some(relative) => format!("{}/{relative}", home.trim_end_matches('/')),
+ None => value.to_owned(),
+ }
+ }
+
+ Self {
+ exec: expand(&self.exec, home),
+ args: self.args.iter().map(|arg| expand(arg, home)).collect(),
+ }
+ }
+}
+
+impl Config {
+ pub fn load(path: Option) -> anyhow::Result {
+ let explicit_path = path.is_some();
+ let path = path.unwrap_or_else(|| path::PathBuf::from("/etc/sessiond-hooks.toml"));
+
+ let config = match fs::read_to_string(&path) {
+ Ok(config) => config,
+ Err(err) if !explicit_path && err.kind() == io::ErrorKind::NotFound => {
+ log::info!(path = path.to_string_lossy().as_ref(); "config file not found, using defaults");
+ anyhow::bail!("");
+ }
+ Err(err) => {
+ log::error!(
+ path = path.to_string_lossy().as_ref(),
+ error = err.to_string().as_str();
+ "failed to read config file"
+ );
+ return Err(err)
+ .with_context(|| format!("failed to read config file {}", path.display()));
+ }
+ };
+
+ toml::from_str::(&config)
+ .inspect_err(|err| {
+ log::error!(
+ path = path.to_string_lossy().as_ref(),
+ error = err.to_string().as_str();
+ "failed to parse config file"
+ );
+ })
+ .with_context(|| format!("failed to parse config file {}", path.display()))
+ }
+}
diff --git a/sessiond-hooks/src/logging.rs b/sessiond-hooks/src/logging.rs
new file mode 100644
index 0000000..5561b43
--- /dev/null
+++ b/sessiond-hooks/src/logging.rs
@@ -0,0 +1,111 @@
+use log::kv;
+use std::fmt::Write as _;
+use std::io::Write as _;
+use std::sync;
+
+type SyslogLogger = syslog::Logger;
+
+/// A syslog logger which preserves the key-values supplied to `log!` macros.
+///
+/// `syslog::BasicLogger` currently formats only `Record::args()`, silently
+/// dropping `Record::key_values()`. Keep the traditional RFC 3164 transport
+/// and put the fields in the message body so existing syslog deployments keep
+/// working.
+pub struct KvLogger {
+ logger: sync::Arc>,
+}
+
+impl KvLogger {
+ fn new(logger: SyslogLogger) -> Self {
+ Self {
+ logger: sync::Arc::new(sync::Mutex::new(logger)),
+ }
+ }
+}
+
+impl log::Log for KvLogger {
+ fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
+ metadata.level() <= log::max_level() && metadata.level() <= log::STATIC_MAX_LEVEL
+ }
+
+ fn log(&self, record: &log::Record<'_>) {
+ if !self.enabled(record.metadata()) {
+ return;
+ }
+
+ let message = format_message(record);
+ let Ok(mut logger) = self.logger.lock() else {
+ return;
+ };
+
+ let result = match record.level() {
+ log::Level::Error => logger.err(message),
+ log::Level::Warn => logger.warning(message),
+ log::Level::Info => logger.info(message),
+ log::Level::Debug | log::Level::Trace => logger.debug(message),
+ };
+ let _ = result;
+ }
+
+ fn flush(&self) {
+ if let Ok(mut logger) = self.logger.lock() {
+ let _ = logger.backend.flush();
+ }
+ }
+}
+
+struct FieldVisitor {
+ fields: String,
+}
+
+impl<'kvs> kv::VisitSource<'kvs> for FieldVisitor {
+ fn visit_pair(&mut self, key: kv::Key<'kvs>, value: kv::Value<'kvs>) -> Result<(), kv::Error> {
+ if !self.fields.is_empty() {
+ self.fields.push(' ');
+ }
+ let _ = write!(self.fields, "{}={}", key.as_str(), value);
+ Ok(())
+ }
+}
+
+pub(crate) fn format_message(record: &log::Record<'_>) -> String {
+ let mut message = record.args().to_string();
+ let mut visitor = FieldVisitor {
+ fields: String::new(),
+ };
+
+ if let Err(error) = record.key_values().visit(&mut visitor) {
+ if !visitor.fields.is_empty() {
+ message.push(' ');
+ }
+ let _ = write!(message, "kv_error={error}");
+ }
+
+ if !visitor.fields.is_empty() {
+ message.push(' ');
+ message.push_str(&visitor.fields);
+ }
+ message
+}
+
+pub fn init_syslog(facility: syslog::Facility, log_level: log::LevelFilter) -> syslog::Result<()> {
+ let formatter = syslog::Formatter3164 {
+ facility,
+ hostname: None,
+ process: "sessiond-hooks".to_owned(),
+ pid: std::process::id(),
+ };
+
+ let logger = if let Ok(logger) = syslog::unix(formatter.clone()) {
+ logger
+ } else if let Ok(logger) = syslog::tcp(formatter.clone(), ("127.0.0.1", 601)) {
+ logger
+ } else {
+ syslog::udp(formatter, ("0.0.0.0", 0), ("127.0.0.1", 514))?
+ };
+
+ log::set_boxed_logger(Box::new(KvLogger::new(logger)))
+ .map_err(|error| syslog::Error::Initialization(Box::new(error)))?;
+ log::set_max_level(log_level);
+ Ok(())
+}
diff --git a/sessiond-hooks/src/main.rs b/sessiond-hooks/src/main.rs
new file mode 100644
index 0000000..4bea48b
--- /dev/null
+++ b/sessiond-hooks/src/main.rs
@@ -0,0 +1,614 @@
+mod config;
+mod logging;
+
+use anyhow::Context;
+use calloop::{generic, timer};
+use clap::{Parser, ValueEnum};
+use hyprwire::client;
+use hyprwire_protocols_sessiond::client::session_core::v1::session_core_v1;
+use hyprwire_protocols_sessiond::client::session_core::v1::session_core_v1::{
+ session_core_manager, session_core_user,
+};
+use hyprwire_protocols_sessiond::client::session_management::v2::session_management_v2;
+use hyprwire_protocols_sessiond::client::session_management::v2::session_management_v2::{
+ session_management_session, session_manager,
+};
+use std::collections::BTreeMap;
+use std::os::fd::AsRawFd;
+use std::os::unix::process::CommandExt;
+use std::{env, ops, path, process, time};
+use uzers::os::unix::UserExt;
+
+#[derive(Parser)]
+#[command(version, about = "Launch and supervise a per-user session hook")]
+struct Cli {
+ #[arg(
+ long,
+ value_name = "PATH",
+ help = "Path to the TOML configuration file"
+ )]
+ config: Option,
+ #[arg(long, value_enum, default_value_t = LogTarget::Stderr)]
+ log_target: LogTarget,
+}
+
+#[derive(Clone, Copy, ValueEnum)]
+enum LogTarget {
+ Stderr,
+ Syslog,
+}
+
+struct PendingManagedSession {
+ id: u32,
+ uid: u32,
+ object: session_management_session::SessionManagementSession,
+ envs: BTreeMap,
+}
+
+struct ManagedSession {
+ id: u32,
+ uid: u32,
+ _object: session_management_session::SessionManagementSession,
+ envs: BTreeMap,
+ hook: Option,
+}
+
+struct User {
+ uid: u32,
+ state: session_core_v1::UserState,
+ state_generation: u64,
+ object: session_core_user::SessionCoreUser,
+}
+
+impl ManagedSession {
+ fn run_hook(
+ &mut self,
+ config: &config::Config,
+ loop_handle: calloop::LoopHandle,
+ ) {
+ let Some(user) = uzers::get_user_by_uid(self.uid) else {
+ log::error!(uid = self.uid; "cannot start hook for unknown uid");
+ return;
+ };
+ let command = config
+ .on_start
+ .expand_home(user.home_dir().to_string_lossy().as_ref());
+ let mut cmd = process::Command::new("/bin/sh");
+ cmd.arg("-c")
+ .arg(
+ r#"
+ [ -r /etc/profile ] && . /etc/profile
+
+ exec env "$@"
+ "#,
+ )
+ .arg("sessiond-hook");
+ for (key, value) in self.envs.iter() {
+ cmd.arg(format!("{key}={value}"));
+ }
+ cmd.arg(&command.exec);
+ for arg in &command.args {
+ cmd.arg(arg);
+ }
+ cmd.uid(self.uid).gid(user.primary_group_id());
+ for (key, value) in self.envs.iter() {
+ cmd.env(key, value);
+ }
+
+ log::info!(
+ exec:? = command.exec,
+ args:? = command.args,
+ session_id = self.id,
+ uid = self.uid,
+ env_count = self.envs.len();
+ "spawning hook"
+ );
+ let mut child = match cmd.spawn() {
+ Ok(child) => child,
+ Err(err) => {
+ log::error!(
+ exec:? = command.exec,
+ uid = self.uid,
+ error:% = err;
+ "failed to spawn hook"
+ );
+ return;
+ }
+ };
+ log::info!(exec:? = command.exec, pid = child.id(),
+ session_id = self.id, uid = self.uid; "spawned hook");
+
+ let Some(pid) = rustix::process::Pid::from_raw(child.id() as i32) else {
+ log::error!(pid = child.id(); "cannot supervise hook with invalid pid");
+ let _ = child.kill();
+ let _ = child.wait();
+ return;
+ };
+ let pidfd = match rustix::process::pidfd_open(pid, rustix::process::PidfdFlags::empty()) {
+ Ok(pidfd) => pidfd,
+ Err(err) => {
+ log::error!(
+ pid = child.id(),
+ error:% = err;
+ "cannot open pidfd for hook"
+ );
+ let _ = child.kill();
+ let _ = child.wait();
+ return;
+ }
+ };
+
+ self.hook = Some(child);
+
+ let fd_wrapper = unsafe { generic::FdWrapper::new(pidfd) };
+ let source = generic::Generic::new(
+ fd_wrapper,
+ calloop::Interest {
+ readable: true,
+ writable: false,
+ },
+ calloop::Mode::Level,
+ );
+
+ let session_id = self.id;
+ if let Err(err) = loop_handle.insert_source(source, move |_, _, state| {
+ let Some(session) = state
+ .managed_sessions
+ .iter_mut()
+ .find(|session| session.id == session_id)
+ else {
+ log::debug!(
+ session_id;
+ "discarding child-exit event for removed session"
+ );
+ return Ok(calloop::PostAction::Remove);
+ };
+
+ if let Some(child) = &mut session.hook
+ && let Some(status) = child.try_wait()?
+ {
+ match (state.config.restart.on, status.success()) {
+ (config::RestartPolicy::Never, _) | (config::RestartPolicy::Failure, true) => {}
+ (config::RestartPolicy::Always, _)
+ | (config::RestartPolicy::Failure, false) => {
+ let restart_delay = state.config.restart.delay;
+ let restart =
+ timer::Timer::from_duration(time::Duration::from_secs(restart_delay));
+ if let Err(error) =
+ state
+ .loop_handle
+ .insert_source(restart, move |_, _, state| {
+ if let Some(session) = state
+ .managed_sessions
+ .iter_mut()
+ .find(|session| session.id == session_id)
+ {
+ log::info!(session_id = session.id,
+ uid = session.uid,
+ delay_seconds = restart_delay;
+ "restarting hook"
+ );
+ session.run_hook(&state.config, state.loop_handle.clone());
+ }
+
+ timer::TimeoutAction::Drop
+ })
+ {
+ log::error!(
+ session_id,
+ error:% = error;
+ "failed to schedule restart"
+ );
+ }
+ }
+ }
+ log::info!(
+ session_id,
+ status:% = status;
+ "hook exited"
+ );
+
+ session.hook = None;
+
+ return Ok(calloop::PostAction::Remove);
+ }
+
+ Ok(calloop::PostAction::Continue)
+ }) {
+ log::error!(
+ session_id,
+ error:% = err;
+ "failed to supervise hook"
+ );
+ if let Some(child) = &mut self.hook {
+ let _ = child.kill();
+ let _ = child.wait();
+ }
+ self.hook = None;
+ }
+ }
+}
+
+fn normal_uid_range(login_defs: &str) -> anyhow::Result> {
+ let mut uid_min = None;
+ let mut uid_max = None;
+
+ for line in login_defs.lines() {
+ let mut fields = line.split_whitespace();
+ let Some(name) = fields.next() else {
+ continue;
+ };
+ let target = match name {
+ "UID_MIN" => &mut uid_min,
+ "UID_MAX" => &mut uid_max,
+ _ => continue,
+ };
+ let value = fields
+ .next()
+ .with_context(|| format!("{name} has no value in /etc/login.defs"))?;
+ *target = Some(
+ value
+ .parse::()
+ .with_context(|| format!("invalid {name} value in /etc/login.defs"))?,
+ );
+ }
+
+ let uid_min = uid_min.context("UID_MIN is missing from /etc/login.defs")?;
+ let uid_max = uid_max.context("UID_MAX is missing from /etc/login.defs")?;
+ anyhow::ensure!(
+ uid_min <= uid_max,
+ "UID_MIN exceeds UID_MAX in /etc/login.defs"
+ );
+
+ Ok(uid_min..=uid_max)
+}
+
+fn is_hook_user(uid: u32, normal_uids: &ops::RangeInclusive) -> bool {
+ uid == 0 || normal_uids.contains(&uid)
+}
+
+struct SessiondHooks {
+ users: Vec,
+ pending_managed_sessions: Vec,
+ managed_sessions: Vec,
+ config: config::Config,
+ normal_uids: ops::RangeInclusive,
+ session_manager: Option,
+ loop_handle: calloop::LoopHandle<'static, Self>,
+}
+
+impl SessiondHooks {
+ fn create_managed_session(&mut self, uid: u32) {
+ if self
+ .managed_sessions
+ .iter()
+ .any(|session| session.uid == uid)
+ || self
+ .pending_managed_sessions
+ .iter()
+ .any(|session| session.uid == uid)
+ {
+ log::info!(uid; "uid already has a managed hook session");
+ return;
+ }
+
+ let Some(session_manager) = self.session_manager.as_ref() else {
+ log::error!(uid; "cannot create managed session: manager is unavailable");
+ return;
+ };
+
+ let Some(session_object) = session_manager.send_create_session::(uid) else {
+ log::error!(uid; "failed to create managed session: protocol returned no object");
+ return;
+ };
+
+ let pending_session = PendingManagedSession {
+ id: 0,
+ uid,
+ object: session_object,
+ envs: BTreeMap::new(),
+ };
+
+ pending_session
+ .object
+ .send_class(session_management_v2::SessionClass::Manager);
+ pending_session.object.send_done();
+
+ self.pending_managed_sessions.push(pending_session);
+ }
+
+ fn finish_teardown(&mut self, uid: u32) {
+ if let Some(index) = self
+ .managed_sessions
+ .iter()
+ .position(|session| session.uid == uid)
+ {
+ let mut session = self.managed_sessions.swap_remove(index);
+ if let Some(mut child) = session.hook.take() {
+ let _ = child.kill();
+ let _ = child.wait();
+ }
+ } else if let Some(index) = self
+ .pending_managed_sessions
+ .iter()
+ .position(|session| session.uid == uid)
+ {
+ drop(self.pending_managed_sessions.swap_remove(index));
+ }
+ }
+}
+
+impl hyprwire::Dispatch for SessiondHooks {
+ fn event(
+ &mut self,
+ object: &session_core_manager::SessionCoreManager,
+ event: ::Event<'_>,
+ ) {
+ match event {
+ session_core_manager::Event::UserAdded { uid } => {
+ if !is_hook_user(uid, &self.normal_uids) {
+ log::debug!(uid; "ignoring non-login user");
+ return;
+ }
+
+ if self.users.iter().any(|user| user.uid == uid) {
+ log::info!(uid; "uid is already being observed");
+ return;
+ }
+
+ let Some(user_object) = object.send_get_user::(uid) else {
+ log::error!(uid; "failed to create user observer: protocol returned no object");
+ return;
+ };
+
+ self.users.push(User {
+ uid,
+ state: session_core_v1::UserState::Online,
+ state_generation: 0,
+ object: user_object,
+ });
+
+ self.create_managed_session(uid);
+ }
+ session_core_manager::Event::Done => {}
+ session_core_manager::Event::Error { code, message } => {
+ log::error!(code:? = code, message:% = message; "session_core_manager error")
+ }
+ _ => {}
+ }
+ }
+}
+
+impl hyprwire::Dispatch for SessiondHooks {
+ fn event(
+ &mut self,
+ object: &session_core_user::SessionCoreUser,
+ event: ::Event<'_>,
+ ) {
+ if let session_core_user::Event::State { state } = event {
+ let Some(user) = self.users.iter_mut().find(|user| user.object == *object) else {
+ return;
+ };
+
+ user.state = state;
+ user.state_generation = user.state_generation.wrapping_add(1);
+ let uid = user.uid;
+ let state_generation = user.state_generation;
+
+ if state == session_core_v1::UserState::Offline {
+ if let Err(error) = self.loop_handle.insert_source(
+ timer::Timer::from_duration(time::Duration::from_secs(5)),
+ {
+ let object = object.clone();
+ move |_, _, state| {
+ let Some(user) = state.users.iter().find(|user| user.object == object)
+ else {
+ return timer::TimeoutAction::Drop;
+ };
+
+ if user.state != session_core_v1::UserState::Offline
+ || user.state_generation != state_generation
+ {
+ return timer::TimeoutAction::Drop;
+ }
+
+ state.finish_teardown(uid);
+ timer::TimeoutAction::Drop
+ }
+ },
+ ) {
+ log::error!(uid, error:% = error; "failed to schedule offline teardown");
+ }
+ } else if matches!(
+ state,
+ session_core_v1::UserState::Lingering
+ | session_core_v1::UserState::Online
+ | session_core_v1::UserState::Active
+ ) {
+ self.create_managed_session(uid);
+ }
+ }
+ }
+}
+
+impl hyprwire::Dispatch for SessiondHooks {
+ fn event(
+ &mut self,
+ object: &session_management_session::SessionManagementSession,
+ event: ::Event<
+ '_,
+ >,
+ ) {
+ match event {
+ session_management_session::Event::Env { name, value } => {
+ let Some(session) = self
+ .pending_managed_sessions
+ .iter_mut()
+ .find(|session| session.object == *object)
+ else {
+ return;
+ };
+
+ log::debug!(name:? = name, session_id = session.id, uid = session.uid;
+ "received environment variable");
+ session.envs.insert(name, value);
+ }
+ session_management_session::Event::SessionId { session_id } => {
+ let Some(session) = self
+ .pending_managed_sessions
+ .iter_mut()
+ .find(|session| session.object == *object)
+ else {
+ return;
+ };
+
+ session.id = session_id;
+ log::info!(session_id, uid = session.uid; "assigned management session object");
+ }
+ session_management_session::Event::Done => {
+ let Some(session) = self
+ .pending_managed_sessions
+ .iter()
+ .position(|session| session.object == *object)
+ else {
+ return;
+ };
+ let session = self.pending_managed_sessions.swap_remove(session);
+
+ let Some(user) = uzers::get_user_by_uid(session.uid) else {
+ log::error!(uid = session.uid; "cannot start hook for unknown uid");
+ return;
+ };
+
+ let mut managed_session = ManagedSession {
+ id: session.id,
+ uid: session.uid,
+ _object: session.object,
+ envs: session.envs,
+ hook: None,
+ };
+ managed_session.envs.extend(BTreeMap::from([
+ (
+ "HOME".to_string(),
+ user.home_dir().to_string_lossy().into_owned(),
+ ),
+ (
+ "USER".to_string(),
+ user.name().to_string_lossy().into_owned(),
+ ),
+ (
+ "LOGNAME".to_string(),
+ user.name().to_string_lossy().into_owned(),
+ ),
+ (
+ "SHELL".to_string(),
+ user.shell().to_string_lossy().into_owned(),
+ ),
+ ]));
+ managed_session.run_hook(&self.config, self.loop_handle.clone());
+
+ self.managed_sessions.push(managed_session);
+ }
+ session_management_session::Event::Error { code, message } => {
+ if let Some(index) = self
+ .pending_managed_sessions
+ .iter()
+ .position(|session| session.object == *object)
+ {
+ let session = self.pending_managed_sessions.swap_remove(index);
+ log::error!(
+ uid = session.uid,
+ session_id = session.id,
+ code:? = code,
+ message:% = message;
+ "managed session creation failed"
+ );
+ }
+ }
+ _ => {}
+ }
+ }
+}
+
+fn main() -> anyhow::Result<()> {
+ let cli = Cli::parse();
+
+ let config = match config::Config::load(cli.config) {
+ Ok(config) => config,
+ Err(err) => {
+ anyhow::bail!(err);
+ }
+ };
+
+ let normal_uids = normal_uid_range(
+ &std::fs::read_to_string("/etc/login.defs").context("reading /etc/login.defs")?,
+ )?;
+ let log_level = match env::var("LOG_LEVEL") {
+ Ok(log) if log == "trace" => log::LevelFilter::Trace,
+ Ok(log) if log == "debug" => log::LevelFilter::Debug,
+ Ok(log) if log == "warn" => log::LevelFilter::Warn,
+ Ok(log) if log == "error" => log::LevelFilter::Error,
+ Ok(log) if log == "off" => log::LevelFilter::Off,
+ _ => log::LevelFilter::Info,
+ };
+
+ match cli.log_target {
+ LogTarget::Stderr => {
+ env_logger::Builder::new()
+ .filter(Some("sessiond_hooks"), log_level)
+ .init();
+ }
+ LogTarget::Syslog => {
+ logging::init_syslog(syslog::Facility::LOG_DAEMON, log_level)?;
+ }
+ }
+
+ let mut event_loop = calloop::EventLoop::try_new()?;
+ let mut socket = client::Client::connect("/run/sessiond/sessiond.sock")?;
+ let eq = socket.new_event_queue();
+
+ let mut sessiond_hooks = SessiondHooks {
+ loop_handle: event_loop.handle(),
+ users: Vec::new(),
+ managed_sessions: Vec::new(),
+ pending_managed_sessions: Vec::new(),
+ normal_uids,
+ session_manager: None,
+ config,
+ };
+
+ socket.add_implementation::();
+ socket.add_implementation::();
+
+ eq.wait_for_handshake(&mut sessiond_hooks)?;
+
+ sessiond_hooks.session_manager =
+ Some(socket.bind::(&eq, &mut sessiond_hooks, 1)?);
+ let _session_core_manager =
+ socket.bind::(&eq, &mut sessiond_hooks, 2)?;
+
+ eq.roundtrip(&mut sessiond_hooks)?;
+
+ let fd_wrapper = unsafe { generic::FdWrapper::new(socket.extract_loop_fd().as_raw_fd()) };
+ let source = generic::Generic::new(
+ fd_wrapper,
+ calloop::Interest {
+ readable: true,
+ writable: false,
+ },
+ calloop::Mode::Level,
+ );
+ event_loop
+ .handle()
+ .insert_source(source, move |_, _, state| {
+ eq.dispatch_events(state, false)
+ .map_err(std::io::Error::other)?;
+ Ok(calloop::PostAction::Continue)
+ })?;
+
+ event_loop.run(None, &mut sessiond_hooks, |_| {})?;
+
+ Ok(())
+}
+
+hyprwire::delegate_noop!(SessiondHooks: session_manager::SessionManager);
diff --git a/sessiond-uaccess/.gitignore b/sessiond-uaccess/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/sessiond-uaccess/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/sessiond-uaccess/Cargo.lock b/sessiond-uaccess/Cargo.lock
new file mode 100644
index 0000000..db5fd92
--- /dev/null
+++ b/sessiond-uaccess/Cargo.lock
@@ -0,0 +1,1503 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "acl-sys"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbc079f9bdd3124fd18df23c67f7e0f79d24751ae151dcffd095fcade07a3eb2"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "bevy_ecs"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a350b2e4c2e47f7903d688affb8a723b31bca63af9789f1da88703f1ba04e354"
+dependencies = [
+ "arrayvec",
+ "bevy_ecs_macros",
+ "bevy_platform",
+ "bevy_ptr",
+ "bevy_tasks",
+ "bevy_utils",
+ "bitflags 2.13.1",
+ "bumpalo",
+ "concurrent-queue",
+ "derive_more",
+ "fixedbitset",
+ "indexmap",
+ "log",
+ "nonmax",
+ "slotmap",
+ "smallvec",
+ "thiserror",
+ "variadics_please",
+]
+
+[[package]]
+name = "bevy_ecs_macro_logic"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a10fef45a3e7dad717ba03e8b5f7ecdf3b3fd1cd1c5e425793525938e90d5f"
+dependencies = [
+ "bevy_macro_utils",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "bevy_ecs_macros"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3916d712264b6f3ec9c964ba10719450ea371e537e202a2ffcfa9f6a5e68169d"
+dependencies = [
+ "bevy_ecs_macro_logic",
+ "bevy_macro_utils",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "bevy_macro_utils"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7164fe229422295bba15f1885048f34f4660db069aacb895208f996b0c7784fb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "toml_edit",
+]
+
+[[package]]
+name = "bevy_platform"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd610a417e7b4a3b9602cf11264d16cbbff8eabe689c5fed8be4e84c6a2680a3"
+dependencies = [
+ "foldhash",
+ "futures-channel",
+ "hashbrown 0.16.1",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+ "spin",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "bevy_ptr"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1974de4b140f102fded747229ff4a61921acec07f02f0d1dcdadfe9ce177d0ff"
+
+[[package]]
+name = "bevy_tasks"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f787d4ba7237b64b30bd2d690a8cbae1ba775e0eec326711c446654cb720e7c"
+dependencies = [
+ "async-task",
+ "atomic-waker",
+ "bevy_platform",
+ "crossbeam-queue",
+ "derive_more",
+ "futures-lite",
+ "heapless",
+ "web-task",
+]
+
+[[package]]
+name = "bevy_utils"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de51fa7ea2e95d4c2d1278c5629285d59eec951cc60c992964c03f30910ef5a1"
+dependencies = [
+ "bevy_platform",
+ "disqualified",
+ "indexmap",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "bstr"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
+dependencies = [
+ "memchr",
+ "serde_core",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "calloop"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
+dependencies = [
+ "bitflags 2.13.1",
+ "nix",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+]
+
+[[package]]
+name = "cc"
+version = "1.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "clap"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+ "portable-atomic",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.119",
+ "unicode-xid",
+]
+
+[[package]]
+name = "disqualified"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd"
+
+[[package]]
+name = "either"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
+
+[[package]]
+name = "env_filter"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "equivalent",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heapless"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
+dependencies = [
+ "hash32",
+ "portable-atomic",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hostname"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-link",
+]
+
+[[package]]
+name = "hyprwire"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ee2af8d569f2a6d71130b9643dc534b7255b57f7953260d044d88d9735e707"
+dependencies = [
+ "hyprwire-core",
+ "hyprwire-scanner",
+ "polling",
+ "rustix",
+]
+
+[[package]]
+name = "hyprwire-core"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a76bc1faef6e638086f116cdfc19e590232d7565f569e01fd9eb217ac86f35f"
+
+[[package]]
+name = "hyprwire-protocols-sessiond"
+version = "0.2.0"
+dependencies = [
+ "hyprwire",
+ "hyprwire-scanner",
+]
+
+[[package]]
+name = "hyprwire-scanner"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b50d1dc744b592f4354885b2c445f7ba4f61e3472d02e23d7b81d73f39dbf15"
+dependencies = [
+ "hyprwire-core",
+ "prettyplease",
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "jiff"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libudev-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "lua-src"
+version = "550.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75c110c2fa33f34e0de05448e1f3eb2e0631e7a69e2d8ae1586cffc9fc9f9949"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "luajit-src"
+version = "210.7.2+b925b3e"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "920cf654b23d217c550ceea57c32cd2a413ea27b6d47ed77b5ee0cf655adefa6"
+dependencies = [
+ "cc",
+ "which",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "mlua"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad72ffa037cf5970c9860674f32f703fda25d86cf217475fe7a79c5f9961bcaa"
+dependencies = [
+ "bstr",
+ "either",
+ "libc",
+ "mlua-sys",
+ "num-traits",
+ "parking_lot",
+ "rustc-hash",
+]
+
+[[package]]
+name = "mlua-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92136787b906d4e55cfe96cd6c62e010bb1a56889d0d6cf83eb016dbad07576b"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "lua-src",
+ "luajit-src",
+ "pkg-config",
+]
+
+[[package]]
+name = "nix"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+dependencies = [
+ "bitflags 2.13.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nonmax"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi 0.5.2",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "posix-acl"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9928b761309e4a4ca4f2d90eb03029142e3f7164107e18db4d46516515b87441"
+dependencies = [
+ "acl-sys",
+ "libc",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.1",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "sessiond-uaccess"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "bevy_ecs",
+ "calloop",
+ "clap",
+ "env_logger",
+ "hyprwire",
+ "hyprwire-protocols-sessiond",
+ "log",
+ "mlua",
+ "posix-acl",
+ "syslog",
+ "udev",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "slotmap"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
+
+[[package]]
+name = "spin"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syslog"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "019f1500a13379b7d051455df397c75770de6311a7a188a699499502704d9f10"
+dependencies = [
+ "hostname",
+ "libc",
+ "log",
+ "time",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.13+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+
+[[package]]
+name = "udev"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f"
+dependencies = [
+ "io-lifetimes",
+ "libc",
+ "libudev-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "variadics_please"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-task"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538"
+dependencies = [
+ "async-task",
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "which"
+version = "8.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
diff --git a/sessiond-uaccess/Cargo.toml b/sessiond-uaccess/Cargo.toml
new file mode 100644
index 0000000..a66b38a
--- /dev/null
+++ b/sessiond-uaccess/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "sessiond-uaccess"
+keywords = []
+categories = []
+edition = "2024"
+license = "GPL-3.0-only"
+repository = "https://tangled.org/r0chd.pl/sessiond"
+version = "0.0.0"
+publish = false
+description = "Dynamic device access manager"
+readme = "README.md"
+
+[dependencies]
+anyhow = "1.0.103"
+calloop = { version = "0.14.4", features = ["signals"] }
+env_logger = { version = "0.11.11", features = ["kv"] }
+hyprwire = "0.5.1"
+hyprwire-protocols-sessiond = { path = "../hyprwire-protocols-sessiond", version = "0.2.0" }
+udev = "0.9.3"
+mlua = { version = "0.12", features = ["lua54", "vendored"] }
+clap = { version = "4.6.2", features = ["derive"] }
+bevy_ecs = { version = "0.19.0", default-features = false }
+log = { version = "0.4.33", features = ["kv"] }
+posix-acl = "1.2.0"
+syslog = "7.0.0"
+[workspace]
diff --git a/sessiond-uaccess/LICENSE b/sessiond-uaccess/LICENSE
new file mode 100644
index 0000000..f20c1b4
--- /dev/null
+++ b/sessiond-uaccess/LICENSE
@@ -0,0 +1,14 @@
+Copyright (C) 2026 Oskar Rochowiak
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
diff --git a/sessiond-uaccess/README.md b/sessiond-uaccess/README.md
new file mode 100644
index 0000000..afaded3
--- /dev/null
+++ b/sessiond-uaccess/README.md
@@ -0,0 +1,98 @@
+# sessiond-uaccess
+
+`sessiond-uaccess` is a `sessiond` client that grants device-node access to
+active local sessions by writing POSIX ACL entries.
+
+## Installation
+
+`sessiond-uaccess` requires `sessiond` to be installed and enabled first.
+
+On finix with flakes:
+
+```nix
+{
+ inputs.sessiond-uaccess.url = "git+https://tangled.org/r0chd.pl/sessiond-uaccess";
+}
+```
+
+```nix
+{ inputs, pkgs, ... }:
+{
+ imports = [
+ inputs.sessiond-uaccess.finixModules.default
+ ];
+
+ services.sessiond-uaccess = {
+ enable = true;
+ package = inputs.sessiond-uaccess.packages.${pkgs.system}.default;
+ };
+}
+```
+
+With npins:
+
+```sh
+npins add git --name sessiond-uaccess https://tangled.org/r0chd.pl/sessiond-uaccess --branch master
+```
+
+```nix
+{ pkgs, ... }:
+let
+ sources = import ./npins;
+ sessiond-uaccess = import sources.sessiond-uaccess.outPath { };
+in
+{
+ imports = [
+ sessiond-uaccess.finixModules.default
+ ];
+
+ services.sessiond-uaccess = {
+ enable = true;
+ package = sessiond-uaccess.packages.sessiond-uaccess;
+ };
+}
+```
+
+To build it manually:
+
+```sh
+cargo build --release
+```
+
+The binary is written to `target/release/sessiond-uaccess`.
+
+## Usage
+
+```sh
+sessiond-uaccess --rules-dirs PATH [--rules-dirs PATH...]
+```
+
+`sessiond-uaccess` connects to `/run/sessiond/sessiond.sock`, loads every `*.lua` file
+from each existing rules directory, scans existing udev devices, and then keeps
+listening for new udev devices and session changes.
+
+### mdevd
+
+`sessiond-uaccess` can be used with `mdevd` and `libudev-zero`. Start `mdevd`
+with `-O 4` so it rebroadcasts processed uevents to the netlink group used by
+the libudev monitor:
+
+```sh
+mdevd -O 4
+```
+
+## Rules
+
+Rules are Lua files that call `device` with an access mode and match function:
+
+```lua
+device {
+ access = "rw",
+ match = function(dev)
+ return dev.subsystem == "sound"
+ end,
+}
+```
+
+The default rules in `rules/70-uaccess.lua` cover sound devices, cameras, DRI
+devices, render and accelerator nodes, AMD KFD, KVM, rfkill, and optical drives.
diff --git a/sessiond-uaccess/lua/sessiond-uaccess.lua b/sessiond-uaccess/lua/sessiond-uaccess.lua
new file mode 100644
index 0000000..b3942d0
--- /dev/null
+++ b/sessiond-uaccess/lua/sessiond-uaccess.lua
@@ -0,0 +1,31 @@
+---@meta sessiond-uaccess
+---@class Device
+---@field subsystem string?
+---@field sysname string
+---@field syspath string
+---@field devnode string?
+local Device = {}
+
+---@param name string
+---@return string?
+function Device:attr(name) end
+
+---@param name string
+---@return string?
+function Device:property(name) end
+
+---@param subsystem? string
+---@return Device?
+function Device:parent(subsystem) end
+
+---@return Device[]
+function Device:parents() end
+
+---@class Rule
+---@field access '"r"'|'"w"'|'"rw"'
+---@field match fun(dev: Device): boolean
+---@param rule Rule
+function device(rule) end
+
+---@type integer
+api_version = 1
diff --git a/sessiond-uaccess/rules/70-uaccess.lua b/sessiond-uaccess/rules/70-uaccess.lua
new file mode 100644
index 0000000..80b22ff
--- /dev/null
+++ b/sessiond-uaccess/rules/70-uaccess.lua
@@ -0,0 +1,90 @@
+-- Sound devices
+device {
+ access = "rw",
+ match = function(dev)
+ return dev.subsystem == "sound"
+ end,
+}
+-- Webcams, frame grabber, TV cards
+device {
+ access = "rw",
+ match = function(dev)
+ return dev.subsystem == "video4linux"
+ or dev.subsystem == "dvb"
+ or dev.subsystem == "media"
+ or dev.sysname == "udmabuf"
+ end,
+}
+
+-- DRI video devices
+device {
+ access = "rw",
+ match = function(dev)
+ return dev.subsystem == "drm"
+ and dev.sysname:match("^card.*$") ~= nil
+ end,
+}
+
+-- DRI render nodes, DRI accel nodes, AMD KFD nodes, KVM
+device {
+ access = "rw",
+ match = function(dev)
+ return (
+ dev.subsystem == "drm"
+ and dev.sysname:match("^renderD.*$") ~= nil
+ ) or (
+ dev.subsystem == "accel"
+ and dev.sysname:match("^accel.*$") ~= nil
+ ) or (
+ dev.subsystem == "kfd"
+ and dev.sysname == "kfd"
+ ) or (
+ dev.subsystem == "misc"
+ and dev.sysname == "kvm"
+ )
+ end,
+}
+
+-- rfkill / radio killswitches
+device {
+ access = "rw",
+ match = function(dev)
+ return dev.sysname == "rfkill"
+ and dev.subsystem == "misc"
+ end,
+}
+
+-- Optical drives
+device {
+ access = "rw",
+ match = function(dev)
+ if dev.subsystem ~= "scsi_generic" then
+ return false
+ end
+
+ local scsi = dev:parent("scsi")
+ if not scsi then
+ return false
+ end
+
+ local ty = scsi:attr("type")
+ return ty == "4" or ty == "5"
+ end,
+}
+
+-- FIDO security keys
+device {
+ access = "rw",
+ match = function(dev)
+ if dev.subsystem ~= "hidraw" then
+ return false
+ end
+ for _, parent in ipairs(dev:parents()) do
+ local product = parent:property("PRODUCT")
+ if product ~= nil and product:match("^1050/") ~= nil then
+ return true
+ end
+ end
+ return false
+ end,
+}
diff --git a/sessiond-uaccess/src/config.rs b/sessiond-uaccess/src/config.rs
new file mode 100644
index 0000000..55130cc
--- /dev/null
+++ b/sessiond-uaccess/src/config.rs
@@ -0,0 +1,184 @@
+use std::{cell, fs, path, rc};
+
+pub struct Config {
+ pub lua: mlua::Lua,
+ pub rules: rc::Rc>>,
+}
+
+impl Config {
+ pub fn load(rule_dirs: &[path::PathBuf]) -> anyhow::Result {
+ let lua = mlua::Lua::new_with(
+ mlua::StdLib::TABLE | mlua::StdLib::STRING | mlua::StdLib::MATH | mlua::StdLib::UTF8,
+ mlua::LuaOptions::default(),
+ )
+ .map_err(|e| anyhow::anyhow!("{e}"))?;
+ lua.set_memory_limit(512 * 1024)
+ .map_err(|e| anyhow::anyhow!("{e}"))?;
+
+ let globals = lua.globals();
+ globals
+ .set("api_version", 1)
+ .map_err(|e| anyhow::anyhow!("{e}"))?;
+
+ let rules = rc::Rc::new(cell::RefCell::new(Vec::new()));
+
+ let device_fn = lua
+ .create_function({
+ let rules = rc::Rc::clone(&rules);
+ move |lua, rule: mlua::Table| {
+ let access = match rule.get::("access")?.as_str() {
+ "r" => AccessMode::Read,
+ "w" => AccessMode::Write,
+ "rw" => AccessMode::ReadWrite,
+ other => {
+ return Err(mlua::Error::RuntimeError(format!(
+ "invalid access mode: {other}"
+ )));
+ }
+ };
+
+ let match_fn =
+ lua.create_registry_value(rule.get::("match")?)?;
+
+ rules.borrow_mut().push(LuaRule { access, match_fn });
+
+ Ok(())
+ }
+ })
+ .map_err(|e| anyhow::anyhow!("{e}"))?;
+
+ lua.globals()
+ .set("device", device_fn)
+ .map_err(|e| anyhow::anyhow!("{e}"))?;
+
+ for dir in rule_dirs.iter().filter(|dir| dir.exists()) {
+ let mut rule_paths = fs::read_dir(dir)?
+ .map(|rule| rule.map(|rule| rule.path()))
+ .collect::, _>>()?;
+ rule_paths.retain(|path| path.extension().is_some_and(|extension| extension == "lua"));
+ rule_paths.sort();
+
+ for path in rule_paths {
+ let source = fs::read_to_string(&path)?;
+
+ lua.load(&source)
+ .set_name(path.to_string_lossy().as_ref())
+ .exec()
+ .map_err(|e| anyhow::anyhow!("{e}"))?;
+
+ log::debug!(path:% = path.display(); "loaded Lua rule file");
+ }
+ }
+
+ Ok(Self { lua, rules })
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum AccessMode {
+ Read,
+ Write,
+ ReadWrite,
+}
+
+impl AccessMode {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Read => "r",
+ Self::Write => "w",
+ Self::ReadWrite => "rw",
+ }
+ }
+}
+
+pub struct LuaRule {
+ pub access: AccessMode,
+ pub match_fn: mlua::RegistryKey,
+}
+
+#[derive(Clone)]
+pub struct LuaDevice {
+ pub device: udev::Device,
+}
+
+impl LuaDevice {
+ pub const fn new(device: udev::Device) -> Self {
+ Self { device }
+ }
+}
+
+impl mlua::UserData for LuaDevice {
+ fn add_fields>(fields: &mut F) {
+ fields.add_field_method_get("subsystem", |_, this| {
+ Ok(this
+ .device
+ .subsystem()
+ .map(|subsystem| subsystem.to_string_lossy().into_owned()))
+ });
+
+ fields.add_field_method_get("sysname", |_, this| {
+ Ok(this.device.sysname().to_string_lossy().into_owned())
+ });
+
+ fields.add_field_method_get("syspath", |_, this| {
+ Ok(this.device.syspath().to_string_lossy().into_owned())
+ });
+
+ fields.add_field_method_get("devnode", |_, this| {
+ Ok(this
+ .device
+ .devnode()
+ .map(|devnode| devnode.to_string_lossy().into_owned()))
+ });
+ }
+
+ fn add_methods>(methods: &mut M) {
+ methods.add_method("attr", |_, this, name: String| {
+ Ok(this
+ .device
+ .attribute_value(&name)
+ .map(|value| value.to_string_lossy().into_owned()))
+ });
+
+ methods.add_method("property", |_, this, name: String| {
+ Ok(this
+ .device
+ .property_value(&name)
+ .map(|value| value.to_string_lossy().into_owned()))
+ });
+
+ methods.add_method("parent", |lua, this, subsystem: Option| {
+ let mut parent = this.device.parent();
+
+ while let Some(device) = parent {
+ let matches = subsystem.as_deref().is_none_or(|subsystem| {
+ device
+ .subsystem()
+ .is_some_and(|device_subsystem| device_subsystem == subsystem)
+ });
+
+ if matches {
+ return lua.create_userdata(Self::new(device)).map(Some);
+ }
+
+ parent = device.parent();
+ }
+
+ Ok(None::)
+ });
+
+ methods.add_method("parents", |lua, this, ()| {
+ let parents = lua.create_table()?;
+ let mut parent = this.device.parent();
+ let mut index = 1;
+
+ while let Some(device) = parent {
+ parents.set(index, Self::new(device.clone()))?;
+ index += 1;
+ parent = device.parent();
+ }
+
+ Ok(parents)
+ });
+ }
+}
diff --git a/sessiond-uaccess/src/device.rs b/sessiond-uaccess/src/device.rs
new file mode 100644
index 0000000..8234df8
--- /dev/null
+++ b/sessiond-uaccess/src/device.rs
@@ -0,0 +1,10 @@
+use crate::ecs;
+use bevy_ecs::prelude::*;
+use std::path;
+
+pub fn entity_by_syspath(world: &mut World, syspath: &path::Path) -> Option {
+ let mut devices = world.query_filtered::<(Entity, &ecs::Syspath), With>();
+ devices
+ .iter(world)
+ .find_map(|(entity, device_syspath)| (device_syspath.0 == syspath).then_some(entity))
+}
diff --git a/sessiond-uaccess/src/ecs.rs b/sessiond-uaccess/src/ecs.rs
new file mode 100644
index 0000000..fe5342d
--- /dev/null
+++ b/sessiond-uaccess/src/ecs.rs
@@ -0,0 +1,59 @@
+use crate::session_core_v1::session_core_session;
+use crate::{config, session_core_v1};
+use bevy_ecs::prelude::*;
+use std::{path, sync};
+
+pub fn insert_session(
+ world: &mut World,
+ object: session_core_session::SessionCoreSession,
+) -> Entity {
+ world
+ .spawn((Session, SessionObject(object), Done(false)))
+ .id()
+}
+
+#[derive(Component)]
+pub struct Syspath(pub sync::Arc);
+
+#[derive(Component)]
+pub struct Devnode(pub path::PathBuf);
+
+#[derive(Component)]
+pub struct Session;
+
+#[derive(Component)]
+pub struct SessionObject(pub session_core_session::SessionCoreSession);
+
+#[derive(Component)]
+pub struct DeviceEntity(pub Entity);
+
+#[derive(Component)]
+pub struct Uid(pub u32);
+
+#[derive(Component)]
+pub struct AccessMode(pub config::AccessMode);
+
+#[derive(Component)]
+pub struct ActiveState(pub session_core_v1::ActiveState);
+
+#[derive(Component)]
+pub struct IsLocal(pub bool);
+
+#[derive(Component)]
+pub struct Done(pub bool);
+
+#[derive(Component)]
+pub struct Device;
+
+#[derive(Component)]
+pub struct Grant;
+
+#[derive(Message)]
+pub struct DeviceRemoved {
+ pub syspath: sync::Arc,
+}
+
+#[derive(Message)]
+pub struct DeviceAdded {
+ pub syspath: sync::Arc,
+}
diff --git a/sessiond-uaccess/src/logging.rs b/sessiond-uaccess/src/logging.rs
new file mode 100644
index 0000000..9981053
--- /dev/null
+++ b/sessiond-uaccess/src/logging.rs
@@ -0,0 +1,136 @@
+use log::kv;
+use std::fmt::Write as _;
+use std::io::Write as _;
+use std::sync;
+
+type SyslogLogger = syslog::Logger;
+
+/// A syslog logger which preserves the key-values supplied to `log!` macros.
+///
+/// `syslog::BasicLogger` currently formats only `Record::args()`, silently
+/// dropping `Record::key_values()`. Keep the traditional RFC 3164 transport
+/// and put the fields in the message body so existing syslog deployments keep
+/// working.
+pub struct KvLogger {
+ logger: sync::Arc>,
+}
+
+impl KvLogger {
+ fn new(logger: SyslogLogger) -> Self {
+ Self {
+ logger: sync::Arc::new(sync::Mutex::new(logger)),
+ }
+ }
+}
+
+impl log::Log for KvLogger {
+ fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
+ metadata.level() <= log::max_level() && metadata.level() <= log::STATIC_MAX_LEVEL
+ }
+
+ fn log(&self, record: &log::Record<'_>) {
+ if !self.enabled(record.metadata()) {
+ return;
+ }
+
+ let message = format_message(record);
+ let Ok(mut logger) = self.logger.lock() else {
+ return;
+ };
+
+ let result = match record.level() {
+ log::Level::Error => logger.err(message),
+ log::Level::Warn => logger.warning(message),
+ log::Level::Info => logger.info(message),
+ log::Level::Debug | log::Level::Trace => logger.debug(message),
+ };
+ let _ = result;
+ }
+
+ fn flush(&self) {
+ if let Ok(mut logger) = self.logger.lock() {
+ let _ = logger.backend.flush();
+ }
+ }
+}
+
+struct FieldVisitor {
+ fields: String,
+}
+
+impl<'kvs> kv::VisitSource<'kvs> for FieldVisitor {
+ fn visit_pair(&mut self, key: kv::Key<'kvs>, value: kv::Value<'kvs>) -> Result<(), kv::Error> {
+ if !self.fields.is_empty() {
+ self.fields.push(' ');
+ }
+ let _ = write!(self.fields, "{}={}", key.as_str(), value);
+ Ok(())
+ }
+}
+
+fn format_message(record: &log::Record<'_>) -> String {
+ let mut message = record.args().to_string();
+ let mut visitor = FieldVisitor {
+ fields: String::new(),
+ };
+
+ if let Err(error) = record.key_values().visit(&mut visitor) {
+ if !visitor.fields.is_empty() {
+ message.push(' ');
+ }
+ let _ = write!(message, "kv_error={error}");
+ }
+
+ if !visitor.fields.is_empty() {
+ message.push(' ');
+ message.push_str(&visitor.fields);
+ }
+ message
+}
+
+pub fn init_syslog(facility: syslog::Facility, log_level: log::LevelFilter) -> syslog::Result<()> {
+ let formatter = syslog::Formatter3164 {
+ facility,
+ hostname: None,
+ process: "sessiond-uaccess".to_owned(),
+ pid: std::process::id(),
+ };
+
+ let logger = if let Ok(logger) = syslog::unix(formatter.clone()) {
+ logger
+ } else if let Ok(logger) = syslog::tcp(formatter.clone(), ("127.0.0.1", 601)) {
+ logger
+ } else {
+ syslog::udp(formatter, ("0.0.0.0", 0), ("127.0.0.1", 514))?
+ };
+
+ log::set_boxed_logger(Box::new(KvLogger::new(logger)))
+ .map_err(|error| syslog::Error::Initialization(Box::new(error)))?;
+ log::set_max_level(log_level);
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::format_message;
+
+ #[test]
+ fn includes_key_values_after_message() {
+ let record = log::Record::builder()
+ .args(format_args!("session added"))
+ .key_values(&[("session_id", 42)])
+ .build();
+
+ assert_eq!(format_message(&record), "session added session_id=42");
+ }
+
+ #[test]
+ fn supports_values_with_spaces() {
+ let record = log::Record::builder()
+ .args(format_args!("assigned"))
+ .key_values(&[("seat", "seat 1")])
+ .build();
+
+ assert_eq!(format_message(&record), "assigned seat=seat 1");
+ }
+}
diff --git a/sessiond-uaccess/src/main.rs b/sessiond-uaccess/src/main.rs
new file mode 100644
index 0000000..3859ccd
--- /dev/null
+++ b/sessiond-uaccess/src/main.rs
@@ -0,0 +1,499 @@
+mod config;
+mod ecs;
+mod logging;
+mod session;
+
+use anyhow::Context;
+use bevy_ecs::message;
+use bevy_ecs::prelude::*;
+use calloop::{generic, signals};
+use clap::{Parser, ValueEnum};
+use hyprwire::client;
+use hyprwire_protocols_sessiond::client::session_core::v1::session_core_v1;
+use posix_acl as acl;
+use session_core_v1::session_core_manager;
+use std::os::fd::AsRawFd;
+use std::{env, path, sync, time};
+
+struct SessiondUaccess {
+ reconcile: Schedule,
+ world: World,
+}
+
+fn revoke_stale_grants(
+ mut commands: Commands,
+ grants: Query<(Entity, &ecs::DeviceEntity, &ecs::AccessMode, &ecs::Uid), With>,
+ devices: Query<(&ecs::Syspath, &ecs::Devnode), With>,
+ sessions: Query<(&ecs::Uid, &ecs::Done, &ecs::IsLocal, &ecs::ActiveState), With>,
+) {
+ for (grant_entity, device_entity, access_mode, grant_uid) in &grants {
+ if sessions.iter().any(|(uid, done, is_local, active)| {
+ uid.0 == grant_uid.0
+ && done.0
+ && is_local.0
+ && active.0 == session_core_v1::ActiveState::Active
+ }) {
+ continue;
+ }
+
+ let Ok((syspath, devnode)) = devices.get(device_entity.0) else {
+ commands.entity(grant_entity).despawn();
+ continue;
+ };
+
+ let mut acl = match acl::PosixACL::read_acl(&devnode.0) {
+ Ok(acl) => acl,
+ Err(e) => {
+ log::error!(
+ syspath:% = syspath.0.display(),
+ devnode:% = devnode.0.display(),
+ uid = grant_uid.0,
+ access = access_mode.0.as_str(),
+ error = e.to_string().as_str();
+ "failed to read device acl"
+ );
+ continue;
+ }
+ };
+ acl.remove(acl::Qualifier::User(grant_uid.0));
+ if let Err(e) = acl.write_acl(&devnode.0) {
+ log::error!(
+ syspath:% = syspath.0.display(),
+ devnode:% = devnode.0.display(),
+ uid = grant_uid.0,
+ access = access_mode.0.as_str(),
+ error = e.to_string().as_str();
+ "failed to write device acl"
+ );
+ continue;
+ }
+
+ commands.entity(grant_entity).despawn();
+
+ log::info!(
+ syspath:% = syspath.0.display(),
+ devnode:% = devnode.0.display(),
+ uid = grant_uid.0,
+ access = access_mode.0.as_str();
+ "removed device grant"
+ );
+ }
+}
+
+fn apply_missing_grants(
+ mut commands: Commands,
+ devices: Query<(Entity, &ecs::Syspath, &ecs::Devnode, &ecs::AccessMode), With>,
+ sessions: Query<(&ecs::Uid, &ecs::Done, &ecs::IsLocal, &ecs::ActiveState), With>,
+ grants: Query<(&ecs::DeviceEntity, &ecs::Uid), With>,
+) {
+ for (uid, done, is_local, active) in &sessions {
+ if !done.0 || !is_local.0 || active.0 != session_core_v1::ActiveState::Active {
+ continue;
+ }
+
+ for (device_entity, syspath, devnode, access_mode) in &devices {
+ let grant_exists = grants.iter().any(|(grant_device_entity, grant_uid)| {
+ grant_device_entity.0 == device_entity && grant_uid.0 == uid.0
+ });
+ if grant_exists {
+ continue;
+ }
+
+ let mut acl = match acl::PosixACL::read_acl(&devnode.0) {
+ Ok(acl) => acl,
+ Err(e) => {
+ log::error!(
+ syspath:% = syspath.0.display(),
+ devnode:% = devnode.0.display(),
+ uid = uid.0,
+ access = access_mode.0.as_str(),
+ error = e.to_string().as_str();
+ "failed to read device acl"
+ );
+ continue;
+ }
+ };
+
+ let acl_rule = match access_mode.0 {
+ config::AccessMode::Read => acl::ACL_READ,
+ config::AccessMode::Write => acl::ACL_WRITE,
+ config::AccessMode::ReadWrite => acl::ACL_READ | acl::ACL_WRITE,
+ };
+ acl.set(acl::Qualifier::User(uid.0), acl_rule);
+ if let Err(e) = acl.write_acl(&devnode.0) {
+ log::error!(
+ syspath:% = syspath.0.display(),
+ devnode:% = devnode.0.display(),
+ uid = uid.0,
+ access = access_mode.0.as_str(),
+ error = e.to_string().as_str();
+ "failed to write device acl"
+ );
+ continue;
+ }
+
+ commands.spawn((
+ ecs::Grant,
+ ecs::DeviceEntity(device_entity),
+ ecs::AccessMode(access_mode.0),
+ ecs::Uid(uid.0),
+ ));
+
+ log::info!(
+ syspath:% = syspath.0.display(),
+ devnode:% = devnode.0.display(),
+ uid = uid.0,
+ access = access_mode.0.as_str();
+ "inserted device grant"
+ );
+ }
+ }
+}
+
+fn process_device_removals(
+ mut message: MessageReader,
+ devices: Query<(Entity, &ecs::Syspath), With>,
+ grants: Query<(Entity, &ecs::DeviceEntity), With>,
+ mut commands: Commands,
+) {
+ for removed in message.read() {
+ let Some((device_entity, _)) = devices
+ .iter()
+ .find(|(_, syspath)| syspath.0.as_ref() == removed.syspath.as_ref())
+ else {
+ continue;
+ };
+
+ for (grant_entity, grant_device) in &grants {
+ if grant_device.0 == device_entity {
+ commands.entity(grant_entity).despawn();
+ }
+ }
+
+ commands.entity(device_entity).despawn();
+ }
+}
+
+#[allow(clippy::needless_pass_by_value)]
+fn reconcile_devices(
+ mut message: MessageReader,
+ mut commands: Commands,
+ devices: Query<
+ (
+ Entity,
+ &ecs::Syspath,
+ Option<&ecs::Devnode>,
+ Option<&ecs::AccessMode>,
+ ),
+ With,
+ >,
+ grants: Query<(Entity, &ecs::DeviceEntity), With>,
+ config: NonSend,
+) {
+ for syspath in message.read().map(|added| sync::Arc::clone(&added.syspath)) {
+ let device = match udev::Device::from_syspath(&syspath) {
+ Ok(device) => config::LuaDevice::new(device),
+ Err(error) => {
+ log::debug!(
+ syspath:% = syspath.display(),
+ error = error.to_string().as_str();
+ "failed to resolve device during reconciliation"
+ );
+ continue;
+ }
+ };
+
+ let started = time::Instant::now();
+ let Some(devnode) = device.device.devnode() else {
+ log::debug!(syspath:% = syspath.display(); "skipping device without devnode");
+ continue;
+ };
+ let subsystem = device
+ .device
+ .subsystem()
+ .map(|subsystem| subsystem.to_string_lossy().into_owned());
+ let sysname = device.device.sysname().to_string_lossy().into_owned();
+
+ for rule in config.rules.borrow().iter() {
+ let result = {
+ let match_fn = config
+ .lua
+ .registry_value::(&rule.match_fn)
+ .map_err(|error| anyhow::anyhow!("{error}"));
+ match_fn.and_then(|match_fn| {
+ match_fn
+ .call::(device.clone())
+ .map(|matched| (matched, rule.access))
+ .map_err(|error| anyhow::anyhow!("{error}"))
+ })
+ };
+
+ let (matched, access) = match result {
+ Ok(result) => result,
+ Err(error) => {
+ log::error!(
+ syspath:% = syspath.display(),
+ error = error.to_string().as_str();
+ "failed to evaluate device match rule"
+ );
+ continue;
+ }
+ };
+
+ if matched {
+ log::debug!(
+ syspath:% = syspath.display(),
+ devnode:% = devnode.display(),
+ subsystem = subsystem.as_deref(),
+ sysname = sysname.as_str();
+ "device matched rule"
+ );
+
+ if let Some((device_entity, _, existing_devnode, existing_access_mode)) = devices
+ .iter()
+ .find(|(_, device_syspath, _, _)| device_syspath.0 == syspath)
+ {
+ let device_changed = existing_devnode
+ .is_none_or(|existing_devnode| existing_devnode.0 != devnode)
+ || existing_access_mode.is_none_or(|access_mode| access_mode.0 != access);
+
+ if device_changed {
+ for (grant_entity, grant_device) in &grants {
+ if grant_device.0 == device_entity {
+ commands.entity(grant_entity).despawn();
+ }
+ }
+ commands.entity(device_entity).despawn();
+ commands.spawn((
+ ecs::Device,
+ ecs::Syspath(sync::Arc::clone(&syspath)),
+ ecs::Devnode(devnode.to_owned()),
+ ecs::AccessMode(access),
+ ));
+ }
+ } else {
+ commands.spawn((
+ ecs::Device,
+ ecs::Syspath(sync::Arc::clone(&syspath)),
+ ecs::Devnode(devnode.to_owned()),
+ ecs::AccessMode(access),
+ ));
+ log::info!(
+ syspath:% = syspath.display(),
+ devnode:% = devnode.display();
+ "inserted managed device"
+ );
+ }
+
+ break;
+ }
+ }
+
+ log::debug!(
+ syspath:% = syspath.display(),
+ devnode:% = devnode.display(),
+ duration:? = started.elapsed();
+ "reconciled device"
+ );
+ }
+}
+
+#[derive(Parser)]
+struct Args {
+ #[arg(long)]
+ rules_dirs: Vec,
+ #[arg(long, value_enum, default_value_t = LogTarget::Stderr)]
+ log_target: LogTarget,
+}
+
+#[derive(Clone, Copy, Debug, ValueEnum)]
+enum LogTarget {
+ Stderr,
+ Syslog,
+}
+
+fn main() -> anyhow::Result<()> {
+ let startup_started = time::Instant::now();
+ let args = Args::parse();
+
+ let log_level = match env::var("LOG_LEVEL") {
+ Ok(log) if log == "trace" => log::LevelFilter::Trace,
+ Ok(log) if log == "debug" => log::LevelFilter::Debug,
+ Ok(log) if log == "warn" => log::LevelFilter::Warn,
+ Ok(log) if log == "error" => log::LevelFilter::Error,
+ Ok(log) if log == "off" => log::LevelFilter::Off,
+ _ => log::LevelFilter::Info,
+ };
+
+ match args.log_target {
+ LogTarget::Stderr => {
+ env_logger::Builder::new()
+ .filter(Some("sessiond_uaccess"), log_level)
+ .init();
+ }
+ LogTarget::Syslog => {
+ logging::init_syslog(syslog::Facility::LOG_DAEMON, log_level)?;
+ }
+ }
+
+ log::info!(target:? = args.log_target, level:? = log_level; "logging initialized");
+
+ let mut schedule = Schedule::default();
+ schedule.add_systems(
+ (
+ message::message_update_system,
+ reconcile_devices,
+ process_device_removals,
+ revoke_stale_grants,
+ apply_missing_grants,
+ )
+ .chain(),
+ );
+
+ let mut world = World::new();
+ message::MessageRegistry::register_message::(&mut world);
+ message::MessageRegistry::register_message::(&mut world);
+ world.insert_non_send(
+ config::Config::load(&args.rules_dirs).context("failed to load Lua rules")?,
+ );
+
+ let mut sessiond_uaccess = SessiondUaccess {
+ world,
+ reconcile: schedule,
+ };
+
+ log::info!(
+ rules_dirs = args.rules_dirs.len(),
+ rules = sessiond_uaccess.world.non_send::().rules.borrow().len();
+ "loaded access rules"
+ );
+
+ let mut event_loop = calloop::EventLoop::try_new()?;
+
+ let loop_signal = event_loop.get_signal();
+
+ let signals = signals::Signals::new(&[signals::Signal::SIGTERM, signals::Signal::SIGINT])?;
+ event_loop
+ .handle()
+ .insert_source(signals, move |event, (), state: &mut SessiondUaccess| {
+ log::info!(signal:? = event.signal(); "terminating");
+
+ let sessions: Vec<_> = {
+ let mut sessions = state
+ .world
+ .query_filtered::<(Entity, &ecs::Uid), With>();
+ sessions
+ .iter(&state.world)
+ .map(|(entity, uid)| (entity, uid.0))
+ .collect()
+ };
+
+ for (session_entity, _) in sessions {
+ state.world.despawn(session_entity);
+ }
+
+ loop_signal.stop();
+ loop_signal.wakeup();
+ })?;
+
+ let mut socket = client::Client::connect("/run/sessiond/sessiond.sock")
+ .context("failed to connect to /run/sessiond/sessiond.sock")?;
+ let eq = socket.new_event_queue();
+
+ socket.add_implementation::();
+
+ eq.wait_for_handshake(&mut sessiond_uaccess)
+ .context("sessiond protocol handshake failed")?;
+
+ let _session_core_manager = socket
+ .bind::(&eq, &mut sessiond_uaccess, 1)
+ .context("failed to bind sessiond manager")?;
+
+ log::info!(socket = "/run/sessiond/sessiond.sock"; "connected to sessiond");
+
+ eq.roundtrip(&mut sessiond_uaccess)?;
+
+ let fd_wrapper = unsafe { generic::FdWrapper::new(socket.extract_loop_fd().as_raw_fd()) };
+ let source = generic::Generic::new(
+ fd_wrapper,
+ calloop::Interest {
+ readable: true,
+ writable: false,
+ },
+ calloop::Mode::Level,
+ );
+ event_loop
+ .handle()
+ .insert_source(source, move |_, _, state| {
+ if let Err(error) = eq.dispatch_events(state, false) {
+ let error = error.to_string();
+ log::error!(error = error.as_str(); "failed to dispatch sessiond events");
+ }
+ Ok(calloop::PostAction::Continue)
+ })?;
+
+ let monitor = udev::MonitorBuilder::new()?.listen()?;
+
+ let mut enumerator = udev::Enumerator::new()?;
+ for device in enumerator.scan_devices()? {
+ log::trace!(
+ syspath = device.syspath().to_string_lossy().as_ref();
+ "reconciling existing device"
+ );
+ sessiond_uaccess.world.write_message(ecs::DeviceAdded {
+ syspath: device.syspath().into(),
+ });
+ }
+
+ event_loop
+ .handle()
+ .insert_source(
+ generic::Generic::new(monitor, calloop::Interest::READ, calloop::Mode::Level),
+ |_, monitor, state: &mut SessiondUaccess| {
+ for event in monitor.iter() {
+ match event.event_type() {
+ udev::EventType::Add | udev::EventType::Change => {
+ state.world.write_message(ecs::DeviceAdded {
+ syspath: event.device().syspath().into(),
+ });
+ }
+ udev::EventType::Remove => {
+ state.world.write_message(ecs::DeviceRemoved {
+ syspath: event.device().syspath().into(),
+ });
+ }
+ _ => {}
+ }
+ }
+
+ Ok(calloop::PostAction::Continue)
+ },
+ )
+ .map_err(|e| anyhow::anyhow!("{e}"))?;
+
+ let wakeup_started = time::Instant::now();
+ event_loop.get_signal().wakeup();
+ log::debug!(
+ duration:? = startup_started.elapsed();
+ "queued initial device reconciliation wakeup"
+ );
+
+ let mut first_schedule = true;
+ event_loop.run(None, &mut sessiond_uaccess, move |state| {
+ let reconciliation_started = time::Instant::now();
+ state.reconcile.run(&mut state.world);
+ let reconciliation_duration = reconciliation_started.elapsed();
+
+ if first_schedule {
+ first_schedule = false;
+ log::debug!(
+ startup_duration:? = startup_started.elapsed(),
+ wakeup_duration:? = wakeup_started.elapsed(),
+ reconciliation_duration:? = reconciliation_duration;
+ "completed startup reconciliation"
+ );
+ }
+ })?;
+
+ Ok(())
+}
diff --git a/sessiond-uaccess/src/session.rs b/sessiond-uaccess/src/session.rs
new file mode 100644
index 0000000..f8bd441
--- /dev/null
+++ b/sessiond-uaccess/src/session.rs
@@ -0,0 +1,71 @@
+use crate::ecs;
+use crate::session_core_v1::{session_core_manager, session_core_session};
+use bevy_ecs::prelude::*;
+
+impl hyprwire::Dispatch for crate::SessiondUaccess {
+ fn event(
+ &mut self,
+ object: &session_core_manager::SessionCoreManager,
+ event: ::Event<'_>,
+ ) {
+ if let session_core_manager::Event::SessionAdded { session_id } = event
+ && let Some(session) = object.send_get_session::(session_id)
+ {
+ ecs::insert_session(&mut self.world, session);
+ log::debug!(session_id; "session added");
+ }
+ }
+}
+
+impl hyprwire::Dispatch for crate::SessiondUaccess {
+ fn event(
+ &mut self,
+ object: &session_core_session::SessionCoreSession,
+ event: ::Event<'_>,
+ ) {
+ let Some(entity) = entity_by_object(&mut self.world, object) else {
+ return;
+ };
+
+ match event {
+ session_core_session::Event::Uid { uid } => {
+ self.world.entity_mut(entity).insert(ecs::Uid(uid));
+ log::debug!(uid; "session UID changed");
+ }
+ session_core_session::Event::Active { state } => {
+ self.world
+ .entity_mut(entity)
+ .insert(ecs::ActiveState(state));
+ log::debug!(state:? = state; "session active state changed");
+ }
+ session_core_session::Event::IsLocal { is_local } => {
+ self.world
+ .entity_mut(entity)
+ .insert(ecs::IsLocal(is_local != 0));
+ log::debug!(is_local = is_local != 0; "session locality changed");
+ }
+ session_core_session::Event::Done => {
+ self.world.entity_mut(entity).insert(ecs::Done(true));
+ log::debug!("session completed");
+ }
+ session_core_session::Event::Destroyed => {
+ let uid = self.world.get::(entity).map(|uid| uid.0);
+
+ self.world.entity_mut(entity).despawn();
+
+ log::info!(uid = uid; "session removed");
+ }
+ _ => {}
+ }
+ }
+}
+
+pub fn entity_by_object(
+ world: &mut World,
+ object: &session_core_session::SessionCoreSession,
+) -> Option {
+ let mut sessions = world.query_filtered::<(Entity, &ecs::SessionObject), With>();
+ sessions
+ .iter(world)
+ .find_map(|(entity, session_object)| (&session_object.0 == object).then_some(entity))
+}
diff --git a/daemon/Cargo.toml b/sessiond/Cargo.toml
similarity index 90%
rename from daemon/Cargo.toml
rename to sessiond/Cargo.toml
index 608d5bd..84944a9 100644
--- a/daemon/Cargo.toml
+++ b/sessiond/Cargo.toml
@@ -1,11 +1,12 @@
[package]
-name = "daemon"
+name = "sessiond"
keywords.workspace = true
categories.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
-version.workspace = true
+version = "0.2.3"
+publish = false
description.workspace = true
readme.workspace = true
@@ -26,9 +27,7 @@ toml = "1.1.2"
uzers = "0.12.2"
syslog = "7.0.0"
hyprwire = { version = "0.5.1", features = ["log"] }
-hyprwire-protocols-sessiond = { path = "../hyprwire-protocols-sessiond", version = "0.1.1", features = [
- "bevy_ecs",
-] }
+hyprwire-protocols-sessiond = { path = "../hyprwire-protocols-sessiond", version = "0.2.0" }
[build-dependencies]
hyprwire-scanner = "0.5.1"
diff --git a/daemon/src/cgroup.rs b/sessiond/src/cgroup.rs
similarity index 99%
rename from daemon/src/cgroup.rs
rename to sessiond/src/cgroup.rs
index cc4dbad..a332467 100644
--- a/daemon/src/cgroup.rs
+++ b/sessiond/src/cgroup.rs
@@ -265,7 +265,7 @@ pub fn handle_cgroup_empty(
user_registry: Res,
cgroup_manager: Res,
cgroups: Query<&CgroupPath, With>,
- config: Res,
+ config: Res,
shutdown_state: Res,
loop_signal: Res,
#[cfg(feature = "consolekit")] console_kit: Res,
@@ -307,7 +307,6 @@ pub fn handle_cgroup_empty(
&& session_registry.entities_by_uid(uid.0).is_empty()
&& let Err(e) = user::remove_runtime_dir(
&config
- .0
.users
.get(&sync::Arc::from(user.name().to_string_lossy()))
.unwrap_or(&default_config)
diff --git a/daemon/src/config.rs b/sessiond/src/config.rs
similarity index 97%
rename from daemon/src/config.rs
rename to sessiond/src/config.rs
index e6321bc..5c61eeb 100644
--- a/daemon/src/config.rs
+++ b/sessiond/src/config.rs
@@ -82,7 +82,7 @@ fn default_runtime_dir() -> String {
"/run/user/{uid}".to_string()
}
-#[derive(serde::Deserialize)]
+#[derive(serde::Deserialize, Resource)]
#[serde(rename_all = "kebab-case")]
#[derive(Default)]
pub struct Config {
@@ -123,6 +123,3 @@ impl Config {
.with_context(|| format!("failed to parse config file {}", path.display()))
}
}
-
-#[derive(Resource)]
-pub struct DaemonConfig(pub Config);
diff --git a/daemon/src/consolekit.rs b/sessiond/src/consolekit.rs
similarity index 100%
rename from daemon/src/consolekit.rs
rename to sessiond/src/consolekit.rs
diff --git a/daemon/src/inhibit.rs b/sessiond/src/inhibit.rs
similarity index 95%
rename from daemon/src/inhibit.rs
rename to sessiond/src/inhibit.rs
index 9e5bf0c..73a28f2 100644
--- a/daemon/src/inhibit.rs
+++ b/sessiond/src/inhibit.rs
@@ -6,6 +6,12 @@ use hyprwire_protocols_sessiond::server::session_inhibit::v1::session_inhibit_v1
use hyprwire_protocols_sessiond::server::session_inhibit::v1::session_inhibit_v1::session_inhibitor;
use std::fs;
+#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Mode(pub session_inhibit_v1::Mode);
+
+#[derive(Component, Debug, Clone, PartialEq, Eq)]
+pub struct SessionInhibitor(pub session_inhibitor::SessionInhibitor);
+
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Inhibitor;
@@ -96,8 +102,8 @@ pub struct InhibitorBundle {
pub username: Username,
pub pid: Pid,
pub comm: Comm,
- pub mode: session_inhibit_v1::Mode,
- pub object: session_inhibitor::SessionInhibitor,
+ pub mode: Mode,
+ pub object: SessionInhibitor,
pub objects: Objects,
}
@@ -189,8 +195,8 @@ impl hyprwire::Dispatch for crat
username: Username(username),
pid: Pid(pid),
comm: Comm(comm),
- mode,
- object: protocol_object,
+ mode: Mode(mode),
+ object: SessionInhibitor(protocol_object),
objects: Objects::default(),
});
@@ -239,12 +245,12 @@ impl hyprwire::Dispatch for crate::Sessiond
let mut inhibitors = self.world.query_filtered::<(
Entity,
&InhibitorId,
- &session_inhibitor::SessionInhibitor,
+ &SessionInhibitor,
&Objects,
), With>();
inhibitors
.iter(&self.world)
- .find(|(_, _, candidate, _)| **candidate == *object)
+ .find(|(_, _, candidate, _)| candidate.0 == *object)
.map(|(entity, id, _, objects)| (entity, *id, objects.0.clone()))
};
@@ -289,7 +295,7 @@ pub fn handle_add_object(
&Username,
&Pid,
&Comm,
- &session_inhibit_v1::Mode,
+ &Mode,
&mut Objects,
),
With,
@@ -313,7 +319,7 @@ pub fn handle_add_object(
request.object.send_comm(&comm.0);
request.object.send_what(what.0);
request.object.send_why(&why.0);
- request.object.send_mode(core_mode(*mode));
+ request.object.send_mode(core_mode(mode.0));
request.object.send_active(1);
request.object.send_done();
objects.0.push(request.object.clone());
diff --git a/daemon/src/main.rs b/sessiond/src/main.rs
similarity index 98%
rename from daemon/src/main.rs
rename to sessiond/src/main.rs
index f66773c..ab63a8b 100644
--- a/daemon/src/main.rs
+++ b/sessiond/src/main.rs
@@ -94,7 +94,6 @@ fn handle_shutdown(
fn run(runtime_dir: &path::Path, config: config::Config) -> anyhow::Result<()> {
let mut event_loop = calloop::EventLoop::try_new()?;
- #[cfg(feature = "consolekit")]
let signals = signals::Signals::new(&[
signals::Signal::SIGUSR1,
signals::Signal::SIGUSR2,
@@ -120,7 +119,7 @@ fn run(runtime_dir: &path::Path, config: config::Config) -> anyhow::Result<()> {
world.init_resource::();
world.init_resource::();
world.insert_resource(cgroup::CgroupManager::new()?);
- world.insert_resource(config::DaemonConfig(config));
+ world.insert_resource(config);
world.insert_resource(LoopSignal(event_loop.get_signal()));
let state_path = runtime_dir.join("state.toml");
world.insert_resource(state::StateWriter(state_path));
@@ -291,7 +290,7 @@ fn main() -> anyhow::Result<()> {
match args.log_target {
LogTarget::Stderr => {
env_logger::Builder::new()
- .filter(Some("daemon"), log_level)
+ .filter(Some("sessiond"), log_level)
.init();
}
LogTarget::Syslog => {
diff --git a/daemon/src/reconcile.rs b/sessiond/src/reconcile.rs
similarity index 77%
rename from daemon/src/reconcile.rs
rename to sessiond/src/reconcile.rs
index ff9404a..ca199d1 100644
--- a/daemon/src/reconcile.rs
+++ b/sessiond/src/reconcile.rs
@@ -3,7 +3,6 @@ use crate::consolekit;
use crate::{session, user};
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::ScheduleLabel;
-use hyprwire_protocols_sessiond::server::session_core::v1::session_core_v1;
use std::mem;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
@@ -38,14 +37,11 @@ pub fn sync_consolekit_sessions(
&session::Uid,
&session::Timestamp,
Option<&session::IsLocal>,
- &session_core_v1::ActiveState,
+ &session::ActiveState,
),
(
With,
- Or<(
- Added,
- Changed,
- )>,
+ Or<(Added, Changed)>,
),
>,
) {
@@ -55,7 +51,7 @@ pub fn sync_consolekit_sessions(
uid: uid.0,
timestamp: timestamp.0,
is_local: is_local.is_some(),
- active: *active_state,
+ active: active_state.0,
});
}
}
@@ -63,27 +59,24 @@ pub fn sync_consolekit_sessions(
pub fn notify_session_active_states(
mut batch: ResMut,
sessions: Query<
- (Entity, &session_core_v1::ActiveState, &session::Objects),
- (
- With,
- Changed,
- ),
+ (Entity, &session::ActiveState, &session::Objects),
+ (With, Changed),
>,
users: Query<
- (Entity, &session_core_v1::UserState, &user::Objects),
- (With, Changed),
+ (Entity, &user::UserState, &user::Objects),
+ (With, Changed),
>,
) {
for (entity, state, objects) in &sessions {
for object in &objects.0 {
- object.send_active(*state);
+ object.send_active(state.0);
}
batch.session(entity);
}
for (entity, state, objects) in &users {
for object in &objects.0 {
- object.send_state(*state);
+ object.send_state(state.0);
}
batch.user(entity);
}
diff --git a/daemon/src/session.rs b/sessiond/src/session.rs
similarity index 97%
rename from daemon/src/session.rs
rename to sessiond/src/session.rs
index 5477bc6..512d7b5 100644
--- a/daemon/src/session.rs
+++ b/sessiond/src/session.rs
@@ -1,5 +1,4 @@
use crate::{cgroup, config, inhibit, user, vt};
-use bevy_ecs::entity;
use bevy_ecs::prelude::*;
use hyprwire::server;
use hyprwire_protocols_sessiond::server::session_core::v1::session_core_v1;
@@ -13,6 +12,21 @@ use hyprwire_protocols_sessiond::server::session_management::v2::session_managem
use std::collections::{BTreeMap, HashMap};
use std::{num, sync, time};
+#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ActiveState(pub session_core_v1::ActiveState);
+
+impl ActiveState {
+ pub const ONLINE: Self = Self(session_core_v1::ActiveState::Online);
+ pub const ACTIVE: Self = Self(session_core_v1::ActiveState::Active);
+ pub const CLOSING: Self = Self(session_core_v1::ActiveState::Closing);
+}
+
+impl From for ActiveState {
+ fn from(state: session_core_v1::ActiveState) -> Self {
+ Self(state)
+ }
+}
+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SessionManagementTarget {
V1(session_manager_v1::SessionManager),
@@ -142,7 +156,7 @@ pub struct SessionBundle {
pub timestamp: Timestamp,
pub leader_id: LeaderId,
pub env: SessionEnv,
- pub active_state: session_core_v1::ActiveState,
+ pub active_state: ActiveState,
pub objects: Objects,
}
@@ -262,7 +276,7 @@ pub fn spawn(
env: BTreeMap, sync::Arc>,
cgroup: cgroup::CgroupBundle,
session_registry: &mut ResMut,
-) -> entity::Entity {
+) -> Entity {
let mut cmd = commands.spawn(SessionBundle {
marker: Session,
id: session_id,
@@ -272,7 +286,7 @@ pub fn spawn(
timestamp: Timestamp(time::SystemTime::now()),
leader_id: LeaderId(leader_id),
env: SessionEnv(env),
- active_state,
+ active_state: active_state.into(),
objects: Objects::default(),
});
cmd.insert(cgroup);
@@ -303,7 +317,7 @@ pub fn spawn(
#[derive(Message)]
pub struct SessionControlMessage {
- pub entity: entity::Entity,
+ pub entity: Entity,
pub object: Option,
pub action: SessionControlAction,
}
@@ -323,7 +337,7 @@ pub fn handle_session_control(
(
&SessionId,
&Uid,
- &mut session_core_v1::ActiveState,
+ &mut ActiveState,
Option<&Vt>,
&mut Objects,
),
@@ -350,7 +364,7 @@ pub fn handle_session_control(
continue;
}
log::info!(session_id = session_id.as_raw(); "terminate requested");
- *active_state = session_core_v1::ActiveState::Closing;
+ *active_state = ActiveState::CLOSING;
log::info!(session_id = session_id.0, vt = vt.map_or(0, |vt| vt.0.get()); "session state changed");
let Ok((cgroup_session_id, cgroup_path)) = cgroups.get(action.entity) else {
continue;
@@ -377,7 +391,7 @@ pub fn handle_session_control(
continue;
}
log::info!(session_id = session_id.as_raw(); "kill requested");
- *active_state = session_core_v1::ActiveState::Closing;
+ *active_state = ActiveState::CLOSING;
log::info!(session_id = session_id.0, vt = vt.map_or(0, |vt| vt.0.get()); "session state changed");
let Ok((cgroup_session_id, cgroup_path)) = cgroups.get(action.entity) else {
continue;
@@ -520,7 +534,7 @@ pub fn handle_add_object(
&Username,
Option<&Vt>,
&SessionClass,
- &session_core_v1::ActiveState,
+ &ActiveState,
&LeaderId,
&Timestamp,
Option<&IsLocal>,
@@ -562,7 +576,7 @@ pub fn handle_add_object(
request.object.send_username(&username.0);
request.object.send_class((*class).into());
request.object.send_vt(vt.map_or(0, |vt| vt.0.get()));
- request.object.send_active(*state);
+ request.object.send_active(state.0);
request.object.send_leader(leader_id.0.cast_unsigned());
let timestamp_secs = timestamp
.0
@@ -734,7 +748,7 @@ pub fn handle_session_management_action(
info_managers: Res,
mut session_registry: ResMut,
sessions: Query<(&SessionEnv, &Uid), With>,
- config: Res,
+ config: Res,
) {
for action in actions.read() {
if let SessionManagementAction::ManagerDestroy { management_object } = &action.action {
@@ -855,7 +869,7 @@ pub fn handle_session_management_action(
}
};
- let user_config = config.0.users.get(&username).cloned().unwrap_or_default();
+ let user_config = config.users.get(&username).cloned().unwrap_or_default();
user::create_runtime_dir(
pending_session.uid,
&user_config.runtime_dir(pending_session.uid),
diff --git a/daemon/src/state.rs b/sessiond/src/state.rs
similarity index 97%
rename from daemon/src/state.rs
rename to sessiond/src/state.rs
index 103cbb5..37b132b 100644
--- a/daemon/src/state.rs
+++ b/sessiond/src/state.rs
@@ -79,7 +79,7 @@ pub fn write_state(
Option<&session::RemoteHost>,
&session::Timestamp,
&session::SessionEnv,
- &session_core_v1::ActiveState,
+ &session::ActiveState,
),
(With, With),
>,
@@ -97,7 +97,7 @@ pub fn write_state(
Changed,
Changed,
Changed,
- Changed,
+ Changed,
)>,
),
>,
@@ -126,7 +126,7 @@ pub fn write_state(
session::SessionClass::Manager => "manager",
session::SessionClass::ManagerEarly => "manager-early",
};
- let active = match active_state {
+ let active = match active_state.0 {
session_core_v1::ActiveState::Active => "active",
session_core_v1::ActiveState::Closing => "closing",
session_core_v1::ActiveState::Opening => "opening",
@@ -203,7 +203,7 @@ pub fn load_state(
state_writer: Res,
mut next_session_id: ResMut,
cgroup_manager: Res,
- config: Res,
+ config: Res,
mut session_registry: ResMut,
) {
if state_writer.0.exists() {
@@ -229,7 +229,6 @@ pub fn load_state(
};
let runtime_dir = config
- .0
.users
.get(&saved_session.username)
.cloned()
diff --git a/daemon/src/terminal.rs b/sessiond/src/terminal.rs
similarity index 100%
rename from daemon/src/terminal.rs
rename to sessiond/src/terminal.rs
diff --git a/daemon/src/user.rs b/sessiond/src/user.rs
similarity index 87%
rename from daemon/src/user.rs
rename to sessiond/src/user.rs
index 3aef1e9..31f1f8a 100644
--- a/daemon/src/user.rs
+++ b/sessiond/src/user.rs
@@ -5,6 +5,23 @@ use hyprwire_protocols_sessiond::server::session_core::v1::session_core_v1::sess
use std::collections::HashMap;
use std::{fs, io, sync};
+#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
+pub struct UserState(pub session_core_v1::UserState);
+
+impl UserState {
+ pub const ACTIVE: Self = Self(session_core_v1::UserState::Active);
+ pub const CLOSING: Self = Self(session_core_v1::UserState::Closing);
+ pub const LINGERING: Self = Self(session_core_v1::UserState::Lingering);
+ pub const OFFLINE: Self = Self(session_core_v1::UserState::Offline);
+ pub const ONLINE: Self = Self(session_core_v1::UserState::Online);
+}
+
+impl From for UserState {
+ fn from(state: session_core_v1::UserState) -> Self {
+ Self(state)
+ }
+}
+
#[derive(Resource, Default)]
pub struct UserRegistry {
pub by_uid: HashMap,
@@ -16,7 +33,7 @@ pub struct UserBundle {
pub marker: User,
pub uid: session::Uid,
pub username: session::Username,
- pub state: session_core_v1::UserState,
+ pub state: UserState,
pub objects: Objects,
}
@@ -34,7 +51,7 @@ pub fn spawn(
uid: u32,
username: sync::Arc,
lingering: bool,
- state: session_core_v1::UserState,
+ state: UserState,
user_registry: &mut ResMut,
info_managers: &session::SessionInfoManagers,
) -> Entity {
@@ -81,11 +98,7 @@ pub fn remove_runtime_dir(path: &str) -> io::Result<()> {
pub fn ensure_session_users(
mut commands: Commands,
sessions: Query<
- (
- &session::Uid,
- &session::Username,
- &session_core_v1::ActiveState,
- ),
+ (&session::Uid, &session::Username, &session::ActiveState),
(With, Added),
>,
mut user_registry: ResMut,
@@ -96,10 +109,10 @@ pub fn ensure_session_users(
continue;
}
- let state = match session_state {
- session_core_v1::ActiveState::Active => session_core_v1::UserState::Active,
- session_core_v1::ActiveState::Closing => session_core_v1::UserState::Closing,
- _ => session_core_v1::UserState::Online,
+ let state = match session_state.0 {
+ session_core_v1::ActiveState::Active => UserState::ACTIVE,
+ session_core_v1::ActiveState::Closing => UserState::CLOSING,
+ _ => UserState::ONLINE,
};
spawn(
@@ -115,20 +128,9 @@ pub fn ensure_session_users(
}
pub fn reconcile_user_states(
- users: Query<
- (
- &session::Uid,
- Option<&Lingering>,
- &mut session_core_v1::UserState,
- ),
- With,
- >,
+ users: Query<(&session::Uid, Option<&Lingering>, &mut UserState), With>,
sessions: Query<
- (
- &session::Uid,
- &session::SessionClass,
- &session_core_v1::ActiveState,
- ),
+ (&session::Uid, &session::SessionClass, &session::ActiveState),
With,
>,
) {
@@ -145,20 +147,20 @@ pub fn reconcile_user_states(
.map(|(_, _, state)| state)
.collect();
- let desired = if states.contains(&&session_core_v1::ActiveState::Active) {
- session_core_v1::UserState::Active
+ let desired = if states.contains(&&session::ActiveState::ACTIVE) {
+ UserState::ACTIVE
} else if !states.is_empty()
&& states
.iter()
- .all(|state| **state == session_core_v1::ActiveState::Closing)
+ .all(|state| **state == session::ActiveState::CLOSING)
{
- session_core_v1::UserState::Closing
+ UserState::CLOSING
} else if !states.is_empty() {
- session_core_v1::UserState::Online
+ UserState::ONLINE
} else if lingering.is_some() {
- session_core_v1::UserState::Lingering
+ UserState::LINGERING
} else {
- session_core_v1::UserState::Offline
+ UserState::OFFLINE
};
if *user_state != desired {
@@ -182,7 +184,7 @@ pub fn handle_add_object(
&session::Uid,
&session::Username,
Option<&Lingering>,
- &session_core_v1::UserState,
+ &UserState,
&mut Objects,
),
With,
@@ -206,7 +208,7 @@ pub fn handle_add_object(
} else {
request.object.send_lingering(0);
}
- request.object.send_state(*state);
+ request.object.send_state(state.0);
request.object.send_can_modify(u32::from(
request.creds.uid.is_root() || uid.0 == request.creds.uid.as_raw(),
@@ -351,13 +353,12 @@ impl hyprwire::Dispatch for crate::Sessiond
pub fn initialize_lingering_users(
mut commands: Commands,
- config: Res,
+ config: Res,
cgroup_manager: Res,
mut user_registry: ResMut,
info_managers: Res,
) {
let lingering_users = config
- .0
.users
.iter()
.filter(|(_, config)| config.linger)
@@ -374,7 +375,7 @@ pub fn initialize_lingering_users(
uid,
user.name().to_string_lossy().into(),
true,
- session_core_v1::UserState::Lingering,
+ UserState::LINGERING,
&mut user_registry,
&info_managers,
);
diff --git a/daemon/src/vt.rs b/sessiond/src/vt.rs
similarity index 71%
rename from daemon/src/vt.rs
rename to sessiond/src/vt.rs
index 3864ebb..c893901 100644
--- a/daemon/src/vt.rs
+++ b/sessiond/src/vt.rs
@@ -1,6 +1,5 @@
use crate::{session, terminal};
use bevy_ecs::prelude::*;
-use hyprwire_protocols_sessiond::server::session_core::v1::session_core_v1;
use std::num;
pub fn current_vt() -> anyhow::Result {
@@ -14,7 +13,7 @@ pub fn refresh_current_vt(
(
&session::Vt,
Option<&session::IsLocal>,
- &mut session_core_v1::ActiveState,
+ &mut session::ActiveState,
),
With,
>,
@@ -24,13 +23,13 @@ pub fn refresh_current_vt(
};
for (vt, is_local, mut active_state) in &mut sessions {
- if *active_state == session_core_v1::ActiveState::Closing {
+ if *active_state == session::ActiveState::CLOSING {
continue;
}
let desired = match (is_local, vt.0 == current_vt) {
- (Some(_), true) => session_core_v1::ActiveState::Active,
- (Some(_), false) => session_core_v1::ActiveState::Online,
+ (Some(_), true) => session::ActiveState::ACTIVE,
+ (Some(_), false) => session::ActiveState::ONLINE,
_ => continue,
};