├── .editorconfig ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci.yml │ └── devskim.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── INSTALL.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── swhkd-svg-bg.svg ├── swhkd-svg.svg └── swhkd.png ├── contrib ├── PKGBUILD └── init │ ├── openrc │ ├── README.md │ └── swhkd │ └── systemd │ ├── README.md │ ├── hotkeys.service │ └── hotkeys.sh ├── docs ├── swhkd-keys.5.scd ├── swhkd.1.scd ├── swhkd.5.scd └── swhks.1.scd ├── flake.lock ├── flake.nix ├── rust-toolchain.toml ├── scripts └── build-polkit-policy.sh ├── swhkd ├── Cargo.toml ├── build.rs └── src │ ├── config.rs │ ├── daemon.rs │ ├── environ.rs │ ├── perms.rs │ └── uinput.rs └── swhks ├── Cargo.toml └── src ├── environ.rs ├── ipc.rs └── main.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | indent_style = tab 7 | indent_size = 4 8 | 9 | [Makefile] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [*.rs] 14 | indent_style = space 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: swhkd bugs 4 | title: "" 5 | labels: bug 6 | assignees: angelofallars, Shinyzenith, EdenQwQ 7 | --- 8 | 9 | **Version Information:** 10 | 11 | - Distribution Information ( run `uname -a` ) 12 | - swhkd version ( `swhkd -V` ) 13 | 14 | **Describe the bug:** 15 | A clear and concise description of what the bug is. 16 | 17 | **Expected behavior:** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Actual behavior:** 21 | A clear and concise description of the behavior. 22 | 23 | **To Reproduce:** 24 | Steps to reproduce the behavior. 25 | 26 | **Additional information:** 27 | Anything else you'd like us to know ? 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Build 17 | run: | 18 | sudo apt-get update 19 | sudo apt-get install -y --no-install-recommends libudev-dev 20 | cargo build --release 21 | 22 | clippy: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | 29 | - name: Clippy 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install -y --no-install-recommends libudev-dev 33 | cargo clippy 34 | 35 | test: 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v2 41 | 42 | - name: Run tests 43 | run: | 44 | sudo apt-get update 45 | sudo apt-get install -y --no-install-recommends libudev-dev 46 | cargo test --verbose 47 | 48 | documentation: 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v2 54 | 55 | - name: Check documentation 56 | run: | 57 | sudo apt update 58 | sudo apt install --no-install-recommends scdoc 59 | for file in $(find . -type f -iwholename "./docs/*.scd"); do scdoc < $file > /dev/null; done 60 | rustfmt: 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v2 66 | 67 | - name: Check formatting 68 | run: | 69 | cargo fmt -- --check 70 | -------------------------------------------------------------------------------- /.github/workflows/devskim.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: DevSkim 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | branches: [ main ] 13 | schedule: 14 | - cron: '39 4 * * 1' 15 | 16 | jobs: 17 | lint: 18 | name: DevSkim 19 | runs-on: ubuntu-20.04 20 | permissions: 21 | actions: read 22 | contents: read 23 | security-events: write 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | 28 | - name: Run DevSkim scanner 29 | uses: microsoft/DevSkim-Action@v1 30 | 31 | - name: Upload DevSkim scan results to GitHub Security tab 32 | uses: github/codeql-action/upload-sarif@v2 33 | with: 34 | sarif_file: devskim-results.sarif 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.zip 3 | *.gz 4 | *.out 5 | com.github.swhkd.pkexec.policy 6 | *rc 7 | !*rc/ 8 | .direnv 9 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition="2021" 2 | newline_style = "Unix" 3 | use_field_init_shorthand = true 4 | use_small_heuristics = "Max" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ### Added 11 | 12 | - New configuration options for modes. These options apply to all keybindings in a mode. 13 | - `swallow` mode option: all keybindings associated with this mode do not emit events 14 | - `oneoff` mode option: automatically exits a mode after using a keybind 15 | - `DESTDIR` variable for the `install` target in the `Makefile` to help 16 | packaging and installation. To install in a subdirectory, just call `make 17 | DESTDIR=subdir install`. 18 | - Detection of added/removed devices (e.g., when plugging or unplugging a 19 | keyboard). The devices are grabbed by `swhkd` if they match the `--device` 20 | parameters if present or if they are recognized as keyboard devices otherwise. 21 | - `Altgr` modifier added (https://github.com/waycrate/swhkd/pull/213). 22 | 23 | ### Changed 24 | 25 | - The project `Makefile` now builds the polkit policy file dynamically depending 26 | on the target installation directories. 27 | - Alt modifier no longer maps to the right aly key. It only maps to the left alt key. Right alt is referred to as Altgr (alt graph). 28 | - Tokio version bumped from 1.23.0 to 1.24.2 (https://github.com/waycrate/swhkd/pull/198). 29 | 30 | ### Fixed 31 | 32 | - Mouse cursors and other devices are no longer blocked when running `swhkd`. 33 | - Option prefixes on modifiers are now properly parsed. e.g., `~control` is now 34 | understood by `swhkd` as the `control` modifier with an option 35 | - Install mandocs in the correct locations. 36 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | aakashsensharma@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to swhkd 2 | 3 | 1. Fork the repo and create your branch from `main`. 4 | 1. Make sure to write tests for the functions you make. 5 | 1. Run `make check` once you're done and ensure the test suite passes. 6 | 7 | ## Any contributions you make will be under the BSD-2-Clause Software License 8 | In short, when you submit code changes, your submissions are understood to be under the same BSD-2-Clause License that covers the project. Feel free to contact the maintainers if that's a concern. 9 | 10 | ## Use a Consistent Coding Style 11 | I'm again borrowing these from [Facebook's Guidelines](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) 12 | 13 | * 4 spaces for indentation. 14 | * You can run `make check` for style unification. 15 | 16 | ## Proper Commit Messages 17 | Make sure to write proper commit messages. 18 | 19 | Example: `[refactor] daemon.rs, simpler IPC implementation`. 20 | 21 | ## License 22 | By contributing, you agree that your contributions will be licensed under its BSD-2-Clause License. 23 | 24 | ## References 25 | This document was adapted from [GitHub Gist](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62). 26 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Simple-Wayland-HotKey-Daemon" 7 | version = "1.3.0-dev" 8 | dependencies = [ 9 | "clap", 10 | "env_logger", 11 | "evdev", 12 | "flate2", 13 | "itertools 0.10.5", 14 | "log", 15 | "nix", 16 | "signal-hook", 17 | "signal-hook-tokio", 18 | "sweet", 19 | "sysinfo", 20 | "tokio", 21 | "tokio-stream", 22 | "tokio-udev", 23 | ] 24 | 25 | [[package]] 26 | name = "addr2line" 27 | version = "0.22.0" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 30 | dependencies = [ 31 | "gimli", 32 | ] 33 | 34 | [[package]] 35 | name = "adler" 36 | version = "1.0.2" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 39 | 40 | [[package]] 41 | name = "aho-corasick" 42 | version = "1.1.3" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 45 | dependencies = [ 46 | "memchr", 47 | ] 48 | 49 | [[package]] 50 | name = "anstream" 51 | version = "0.6.15" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 54 | dependencies = [ 55 | "anstyle", 56 | "anstyle-parse", 57 | "anstyle-query", 58 | "anstyle-wincon", 59 | "colorchoice", 60 | "is_terminal_polyfill", 61 | "utf8parse", 62 | ] 63 | 64 | [[package]] 65 | name = "anstyle" 66 | version = "1.0.8" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 69 | 70 | [[package]] 71 | name = "anstyle-parse" 72 | version = "0.2.5" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 75 | dependencies = [ 76 | "utf8parse", 77 | ] 78 | 79 | [[package]] 80 | name = "anstyle-query" 81 | version = "1.1.1" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 84 | dependencies = [ 85 | "windows-sys", 86 | ] 87 | 88 | [[package]] 89 | name = "anstyle-wincon" 90 | version = "3.0.4" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 93 | dependencies = [ 94 | "anstyle", 95 | "windows-sys", 96 | ] 97 | 98 | [[package]] 99 | name = "anyhow" 100 | version = "1.0.86" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 103 | 104 | [[package]] 105 | name = "atty" 106 | version = "0.2.14" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 109 | dependencies = [ 110 | "hermit-abi 0.1.19", 111 | "libc", 112 | "winapi", 113 | ] 114 | 115 | [[package]] 116 | name = "autocfg" 117 | version = "1.3.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 120 | 121 | [[package]] 122 | name = "backtrace" 123 | version = "0.3.73" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 126 | dependencies = [ 127 | "addr2line", 128 | "cc", 129 | "cfg-if", 130 | "libc", 131 | "miniz_oxide", 132 | "object", 133 | "rustc-demangle", 134 | ] 135 | 136 | [[package]] 137 | name = "bitflags" 138 | version = "1.3.2" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 141 | 142 | [[package]] 143 | name = "bitflags" 144 | version = "2.6.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 147 | 148 | [[package]] 149 | name = "bitvec" 150 | version = "1.0.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 153 | dependencies = [ 154 | "funty", 155 | "radium", 156 | "tap", 157 | "wyz", 158 | ] 159 | 160 | [[package]] 161 | name = "block-buffer" 162 | version = "0.10.4" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 165 | dependencies = [ 166 | "generic-array", 167 | ] 168 | 169 | [[package]] 170 | name = "bytes" 171 | version = "1.6.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" 174 | 175 | [[package]] 176 | name = "cc" 177 | version = "1.1.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" 180 | 181 | [[package]] 182 | name = "cfg-if" 183 | version = "1.0.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 186 | 187 | [[package]] 188 | name = "clap" 189 | version = "4.5.11" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" 192 | dependencies = [ 193 | "clap_builder", 194 | "clap_derive", 195 | ] 196 | 197 | [[package]] 198 | name = "clap_builder" 199 | version = "4.5.11" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" 202 | dependencies = [ 203 | "anstream", 204 | "anstyle", 205 | "clap_lex", 206 | "strsim", 207 | ] 208 | 209 | [[package]] 210 | name = "clap_derive" 211 | version = "4.5.11" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" 214 | dependencies = [ 215 | "heck", 216 | "proc-macro2", 217 | "quote", 218 | "syn", 219 | ] 220 | 221 | [[package]] 222 | name = "clap_lex" 223 | version = "0.7.2" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 226 | 227 | [[package]] 228 | name = "colorchoice" 229 | version = "1.0.2" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 232 | 233 | [[package]] 234 | name = "core-foundation-sys" 235 | version = "0.8.6" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 238 | 239 | [[package]] 240 | name = "cpufeatures" 241 | version = "0.2.12" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 244 | dependencies = [ 245 | "libc", 246 | ] 247 | 248 | [[package]] 249 | name = "crc32fast" 250 | version = "1.4.2" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 253 | dependencies = [ 254 | "cfg-if", 255 | ] 256 | 257 | [[package]] 258 | name = "crossbeam-deque" 259 | version = "0.8.5" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 262 | dependencies = [ 263 | "crossbeam-epoch", 264 | "crossbeam-utils", 265 | ] 266 | 267 | [[package]] 268 | name = "crossbeam-epoch" 269 | version = "0.9.18" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 272 | dependencies = [ 273 | "crossbeam-utils", 274 | ] 275 | 276 | [[package]] 277 | name = "crossbeam-utils" 278 | version = "0.8.20" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 281 | 282 | [[package]] 283 | name = "crypto-common" 284 | version = "0.1.6" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 287 | dependencies = [ 288 | "generic-array", 289 | "typenum", 290 | ] 291 | 292 | [[package]] 293 | name = "digest" 294 | version = "0.10.7" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 297 | dependencies = [ 298 | "block-buffer", 299 | "crypto-common", 300 | ] 301 | 302 | [[package]] 303 | name = "either" 304 | version = "1.13.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 307 | 308 | [[package]] 309 | name = "env_logger" 310 | version = "0.9.3" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" 313 | dependencies = [ 314 | "atty", 315 | "humantime", 316 | "log", 317 | "regex", 318 | "termcolor", 319 | ] 320 | 321 | [[package]] 322 | name = "evdev" 323 | version = "0.12.2" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "ab6055a93a963297befb0f4f6e18f314aec9767a4bbe88b151126df2433610a7" 326 | dependencies = [ 327 | "bitvec", 328 | "cfg-if", 329 | "futures-core", 330 | "libc", 331 | "nix", 332 | "thiserror", 333 | "tokio", 334 | ] 335 | 336 | [[package]] 337 | name = "flate2" 338 | version = "1.0.30" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" 341 | dependencies = [ 342 | "crc32fast", 343 | "miniz_oxide", 344 | ] 345 | 346 | [[package]] 347 | name = "funty" 348 | version = "2.0.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 351 | 352 | [[package]] 353 | name = "futures-core" 354 | version = "0.3.30" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 357 | 358 | [[package]] 359 | name = "generic-array" 360 | version = "0.14.7" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 363 | dependencies = [ 364 | "typenum", 365 | "version_check", 366 | ] 367 | 368 | [[package]] 369 | name = "gimli" 370 | version = "0.29.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 373 | 374 | [[package]] 375 | name = "heck" 376 | version = "0.5.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 379 | 380 | [[package]] 381 | name = "hermit-abi" 382 | version = "0.1.19" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 385 | dependencies = [ 386 | "libc", 387 | ] 388 | 389 | [[package]] 390 | name = "hermit-abi" 391 | version = "0.3.9" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 394 | 395 | [[package]] 396 | name = "humantime" 397 | version = "2.1.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 400 | 401 | [[package]] 402 | name = "is_terminal_polyfill" 403 | version = "1.70.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 406 | 407 | [[package]] 408 | name = "itertools" 409 | version = "0.10.5" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 412 | dependencies = [ 413 | "either", 414 | ] 415 | 416 | [[package]] 417 | name = "itertools" 418 | version = "0.12.1" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 421 | dependencies = [ 422 | "either", 423 | ] 424 | 425 | [[package]] 426 | name = "libc" 427 | version = "0.2.159" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" 430 | 431 | [[package]] 432 | name = "libudev-sys" 433 | version = "0.1.4" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" 436 | dependencies = [ 437 | "libc", 438 | "pkg-config", 439 | ] 440 | 441 | [[package]] 442 | name = "lock_api" 443 | version = "0.4.12" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 446 | dependencies = [ 447 | "autocfg", 448 | "scopeguard", 449 | ] 450 | 451 | [[package]] 452 | name = "log" 453 | version = "0.4.22" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 456 | 457 | [[package]] 458 | name = "memchr" 459 | version = "2.7.4" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 462 | 463 | [[package]] 464 | name = "memoffset" 465 | version = "0.6.5" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 468 | dependencies = [ 469 | "autocfg", 470 | ] 471 | 472 | [[package]] 473 | name = "miniz_oxide" 474 | version = "0.7.4" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 477 | dependencies = [ 478 | "adler", 479 | ] 480 | 481 | [[package]] 482 | name = "mio" 483 | version = "1.0.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 486 | dependencies = [ 487 | "hermit-abi 0.3.9", 488 | "libc", 489 | "wasi", 490 | "windows-sys", 491 | ] 492 | 493 | [[package]] 494 | name = "nix" 495 | version = "0.23.2" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" 498 | dependencies = [ 499 | "bitflags 1.3.2", 500 | "cc", 501 | "cfg-if", 502 | "libc", 503 | "memoffset", 504 | ] 505 | 506 | [[package]] 507 | name = "ntapi" 508 | version = "0.3.7" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" 511 | dependencies = [ 512 | "winapi", 513 | ] 514 | 515 | [[package]] 516 | name = "object" 517 | version = "0.36.2" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" 520 | dependencies = [ 521 | "memchr", 522 | ] 523 | 524 | [[package]] 525 | name = "once_cell" 526 | version = "1.19.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 529 | 530 | [[package]] 531 | name = "parking_lot" 532 | version = "0.12.3" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 535 | dependencies = [ 536 | "lock_api", 537 | "parking_lot_core", 538 | ] 539 | 540 | [[package]] 541 | name = "parking_lot_core" 542 | version = "0.9.10" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 545 | dependencies = [ 546 | "cfg-if", 547 | "libc", 548 | "redox_syscall", 549 | "smallvec", 550 | "windows-targets", 551 | ] 552 | 553 | [[package]] 554 | name = "pest" 555 | version = "2.7.11" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" 558 | dependencies = [ 559 | "memchr", 560 | "thiserror", 561 | "ucd-trie", 562 | ] 563 | 564 | [[package]] 565 | name = "pest_derive" 566 | version = "2.7.11" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" 569 | dependencies = [ 570 | "pest", 571 | "pest_generator", 572 | ] 573 | 574 | [[package]] 575 | name = "pest_generator" 576 | version = "2.7.11" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" 579 | dependencies = [ 580 | "pest", 581 | "pest_meta", 582 | "proc-macro2", 583 | "quote", 584 | "syn", 585 | ] 586 | 587 | [[package]] 588 | name = "pest_meta" 589 | version = "2.7.11" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" 592 | dependencies = [ 593 | "once_cell", 594 | "pest", 595 | "sha2", 596 | ] 597 | 598 | [[package]] 599 | name = "pin-project-lite" 600 | version = "0.2.14" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 603 | 604 | [[package]] 605 | name = "pkg-config" 606 | version = "0.3.30" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 609 | 610 | [[package]] 611 | name = "proc-macro2" 612 | version = "1.0.86" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 615 | dependencies = [ 616 | "unicode-ident", 617 | ] 618 | 619 | [[package]] 620 | name = "quote" 621 | version = "1.0.36" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 624 | dependencies = [ 625 | "proc-macro2", 626 | ] 627 | 628 | [[package]] 629 | name = "radium" 630 | version = "0.7.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 633 | 634 | [[package]] 635 | name = "rayon" 636 | version = "1.10.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 639 | dependencies = [ 640 | "either", 641 | "rayon-core", 642 | ] 643 | 644 | [[package]] 645 | name = "rayon-core" 646 | version = "1.12.1" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 649 | dependencies = [ 650 | "crossbeam-deque", 651 | "crossbeam-utils", 652 | ] 653 | 654 | [[package]] 655 | name = "redox_syscall" 656 | version = "0.5.3" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 659 | dependencies = [ 660 | "bitflags 2.6.0", 661 | ] 662 | 663 | [[package]] 664 | name = "regex" 665 | version = "1.10.5" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 668 | dependencies = [ 669 | "aho-corasick", 670 | "memchr", 671 | "regex-automata", 672 | "regex-syntax", 673 | ] 674 | 675 | [[package]] 676 | name = "regex-automata" 677 | version = "0.4.7" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 680 | dependencies = [ 681 | "aho-corasick", 682 | "memchr", 683 | "regex-syntax", 684 | ] 685 | 686 | [[package]] 687 | name = "regex-syntax" 688 | version = "0.8.4" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 691 | 692 | [[package]] 693 | name = "rustc-demangle" 694 | version = "0.1.24" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 697 | 698 | [[package]] 699 | name = "scopeguard" 700 | version = "1.2.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 703 | 704 | [[package]] 705 | name = "sha2" 706 | version = "0.10.8" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 709 | dependencies = [ 710 | "cfg-if", 711 | "cpufeatures", 712 | "digest", 713 | ] 714 | 715 | [[package]] 716 | name = "signal-hook" 717 | version = "0.3.17" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 720 | dependencies = [ 721 | "libc", 722 | "signal-hook-registry", 723 | ] 724 | 725 | [[package]] 726 | name = "signal-hook-registry" 727 | version = "1.4.2" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 730 | dependencies = [ 731 | "libc", 732 | ] 733 | 734 | [[package]] 735 | name = "signal-hook-tokio" 736 | version = "0.3.1" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" 739 | dependencies = [ 740 | "futures-core", 741 | "libc", 742 | "signal-hook", 743 | "tokio", 744 | ] 745 | 746 | [[package]] 747 | name = "smallvec" 748 | version = "1.13.2" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 751 | 752 | [[package]] 753 | name = "socket2" 754 | version = "0.5.7" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 757 | dependencies = [ 758 | "libc", 759 | "windows-sys", 760 | ] 761 | 762 | [[package]] 763 | name = "strsim" 764 | version = "0.11.1" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 767 | 768 | [[package]] 769 | name = "sweet" 770 | version = "0.4.0" 771 | source = "git+https://github.com/waycrate/sweet.git#797d88bfefe8242b96da6b20afa40d489d3ec076" 772 | dependencies = [ 773 | "anyhow", 774 | "bitflags 2.6.0", 775 | "evdev", 776 | "itertools 0.12.1", 777 | "pest", 778 | "pest_derive", 779 | "thiserror", 780 | ] 781 | 782 | [[package]] 783 | name = "swhks" 784 | version = "1.3.0-dev" 785 | dependencies = [ 786 | "clap", 787 | "env_logger", 788 | "log", 789 | "nix", 790 | "sysinfo", 791 | ] 792 | 793 | [[package]] 794 | name = "syn" 795 | version = "2.0.72" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 798 | dependencies = [ 799 | "proc-macro2", 800 | "quote", 801 | "unicode-ident", 802 | ] 803 | 804 | [[package]] 805 | name = "sysinfo" 806 | version = "0.23.13" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "3977ec2e0520829be45c8a2df70db2bf364714d8a748316a10c3c35d4d2b01c9" 809 | dependencies = [ 810 | "cfg-if", 811 | "core-foundation-sys", 812 | "libc", 813 | "ntapi", 814 | "once_cell", 815 | "rayon", 816 | "winapi", 817 | ] 818 | 819 | [[package]] 820 | name = "tap" 821 | version = "1.0.1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 824 | 825 | [[package]] 826 | name = "termcolor" 827 | version = "1.4.1" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 830 | dependencies = [ 831 | "winapi-util", 832 | ] 833 | 834 | [[package]] 835 | name = "thiserror" 836 | version = "1.0.63" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 839 | dependencies = [ 840 | "thiserror-impl", 841 | ] 842 | 843 | [[package]] 844 | name = "thiserror-impl" 845 | version = "1.0.63" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 848 | dependencies = [ 849 | "proc-macro2", 850 | "quote", 851 | "syn", 852 | ] 853 | 854 | [[package]] 855 | name = "tokio" 856 | version = "1.39.2" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" 859 | dependencies = [ 860 | "backtrace", 861 | "bytes", 862 | "libc", 863 | "mio", 864 | "parking_lot", 865 | "pin-project-lite", 866 | "signal-hook-registry", 867 | "socket2", 868 | "tokio-macros", 869 | "windows-sys", 870 | ] 871 | 872 | [[package]] 873 | name = "tokio-macros" 874 | version = "2.4.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 877 | dependencies = [ 878 | "proc-macro2", 879 | "quote", 880 | "syn", 881 | ] 882 | 883 | [[package]] 884 | name = "tokio-stream" 885 | version = "0.1.15" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" 888 | dependencies = [ 889 | "futures-core", 890 | "pin-project-lite", 891 | "tokio", 892 | ] 893 | 894 | [[package]] 895 | name = "tokio-udev" 896 | version = "0.9.1" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "f25418da261774ef3dcae985951bc138cf5fd49b3f4bd7450124ca75af8ed142" 899 | dependencies = [ 900 | "futures-core", 901 | "tokio", 902 | "udev", 903 | ] 904 | 905 | [[package]] 906 | name = "typenum" 907 | version = "1.17.0" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 910 | 911 | [[package]] 912 | name = "ucd-trie" 913 | version = "0.1.6" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 916 | 917 | [[package]] 918 | name = "udev" 919 | version = "0.7.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "4ebdbbd670373442a12fe9ef7aeb53aec4147a5a27a00bbc3ab639f08f48191a" 922 | dependencies = [ 923 | "libc", 924 | "libudev-sys", 925 | "pkg-config", 926 | ] 927 | 928 | [[package]] 929 | name = "unicode-ident" 930 | version = "1.0.12" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 933 | 934 | [[package]] 935 | name = "utf8parse" 936 | version = "0.2.2" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 939 | 940 | [[package]] 941 | name = "version_check" 942 | version = "0.9.5" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 945 | 946 | [[package]] 947 | name = "wasi" 948 | version = "0.11.0+wasi-snapshot-preview1" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 951 | 952 | [[package]] 953 | name = "winapi" 954 | version = "0.3.9" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 957 | dependencies = [ 958 | "winapi-i686-pc-windows-gnu", 959 | "winapi-x86_64-pc-windows-gnu", 960 | ] 961 | 962 | [[package]] 963 | name = "winapi-i686-pc-windows-gnu" 964 | version = "0.4.0" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 967 | 968 | [[package]] 969 | name = "winapi-util" 970 | version = "0.1.8" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 973 | dependencies = [ 974 | "windows-sys", 975 | ] 976 | 977 | [[package]] 978 | name = "winapi-x86_64-pc-windows-gnu" 979 | version = "0.4.0" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 982 | 983 | [[package]] 984 | name = "windows-sys" 985 | version = "0.52.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 988 | dependencies = [ 989 | "windows-targets", 990 | ] 991 | 992 | [[package]] 993 | name = "windows-targets" 994 | version = "0.52.6" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 997 | dependencies = [ 998 | "windows_aarch64_gnullvm", 999 | "windows_aarch64_msvc", 1000 | "windows_i686_gnu", 1001 | "windows_i686_gnullvm", 1002 | "windows_i686_msvc", 1003 | "windows_x86_64_gnu", 1004 | "windows_x86_64_gnullvm", 1005 | "windows_x86_64_msvc", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "windows_aarch64_gnullvm" 1010 | version = "0.52.6" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1013 | 1014 | [[package]] 1015 | name = "windows_aarch64_msvc" 1016 | version = "0.52.6" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1019 | 1020 | [[package]] 1021 | name = "windows_i686_gnu" 1022 | version = "0.52.6" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1025 | 1026 | [[package]] 1027 | name = "windows_i686_gnullvm" 1028 | version = "0.52.6" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1031 | 1032 | [[package]] 1033 | name = "windows_i686_msvc" 1034 | version = "0.52.6" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1037 | 1038 | [[package]] 1039 | name = "windows_x86_64_gnu" 1040 | version = "0.52.6" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1043 | 1044 | [[package]] 1045 | name = "windows_x86_64_gnullvm" 1046 | version = "0.52.6" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1049 | 1050 | [[package]] 1051 | name = "windows_x86_64_msvc" 1052 | version = "0.52.6" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1055 | 1056 | [[package]] 1057 | name = "wyz" 1058 | version = "0.5.1" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1061 | dependencies = [ 1062 | "tap", 1063 | ] 1064 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "swhkd", 4 | "swhks" 5 | ] 6 | 7 | [profile.release] 8 | lto = true # Enable Link Time Optimization 9 | codegen-units = 1 # Reduce number of codegen units to increase optimizations. 10 | panic = 'abort' # Abort on panic 11 | strip = true # Strip symbols from binary* 12 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # AUR: 2 | 3 | We have packaged `swhkd-git`. `swhkd-bin` has been packaged separately by a user of swhkd. 4 | 5 | # Building: 6 | 7 | `swhkd` and `swhks` install to `/usr/local/bin/` by default. You can change this behaviour by editing the [Makefile](../Makefile) variable, `DESTDIR`, which acts as a prefix for all installed files. You can also specify it in the make command line, e.g. to install everything in `subdir`: `make DESTDIR="subdir" install`. 8 | 9 | Note: On some systems swhkd daemon might disable wifi due to issues with rfkill, you could pass `make NO_RFKILL_SW_SUPPORT=1` while buliding to disable rfkill support. 10 | 11 | # Dependencies: 12 | 13 | **Runtime:** 14 | 15 | - Uinput kernel module 16 | - Evdev kernel module 17 | 18 | **Compile time:** 19 | 20 | - git 21 | - scdoc (If present, man-pages will be generated) 22 | - make 23 | - libudev (in Debian, the package name is `libudev-dev`) 24 | - rustup 25 | 26 | # Compiling: 27 | 28 | - `git clone https://github.com/waycrate/swhkd;cd swhkd` 29 | - `make setup` 30 | - `make clean` 31 | - `make` 32 | - `sudo make install` 33 | 34 | # Running: 35 | 36 | Refer [Running section](https://github.com/waycrate/swhkd#running) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Aakash Sen Sharma & Contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Destination dir, defaults to root. Should be overridden for packaging 2 | # e.g. make DESTDIR="packaging_subdir" install 3 | DESTDIR ?= "/" 4 | DAEMON_BINARY := swhkd 5 | SERVER_BINARY := swhks 6 | BUILDFLAGS := --release 7 | TARGET_DIR := /usr/bin 8 | MAN1_DIR := /usr/share/man/man1 9 | MAN5_DIR := /usr/share/man/man5 10 | VERSION = $(shell awk -F ' = ' '$$1 ~ /version/ { gsub(/["]/, "", $$2); printf("%s",$$2) }' Cargo.toml) 11 | 12 | ifneq ($(NO_RFKILL_SW_SUPPORT),) 13 | BUILDFLAGS += --features "no_rfkill" 14 | endif 15 | 16 | all: build 17 | 18 | build: 19 | @cargo build $(BUILDFLAGS) 20 | 21 | install: 22 | @find ./docs -type f -iname "*.1.gz" \ 23 | -exec install -Dm 644 {} -t $(DESTDIR)/$(MAN1_DIR) \; 24 | @find ./docs -type f -iname "*.5.gz" \ 25 | -exec install -Dm 644 {} -t $(DESTDIR)/$(MAN5_DIR) \; 26 | @install -Dm 755 ./target/release/$(DAEMON_BINARY) -t $(DESTDIR)/$(TARGET_DIR) 27 | @sudo chown root:root $(DESTDIR)/$(TARGET_DIR)/$(DAEMON_BINARY) 28 | @sudo chmod u+s $(DESTDIR)/$(TARGET_DIR)/$(DAEMON_BINARY) 29 | @install -Dm 755 ./target/release/$(SERVER_BINARY) -t $(DESTDIR)/$(TARGET_DIR) 30 | # Ideally, we would have a default config file instead of an empty one 31 | @if [ ! -f $(DESTDIR)/etc/$(DAEMON_BINARY)/$(DAEMON_BINARY)rc ]; then \ 32 | touch ./$(DAEMON_BINARY)rc; \ 33 | install -Dm 644 ./$(DAEMON_BINARY)rc -t $(DESTDIR)/etc/$(DAEMON_BINARY); \ 34 | fi 35 | 36 | uninstall: 37 | @$(RM) -f /usr/share/man/**/swhkd.* 38 | @$(RM) -f /usr/share/man/**/swhks.* 39 | @$(RM) $(TARGET_DIR)/$(SERVER_BINARY) 40 | @$(RM) $(TARGET_DIR)/$(DAEMON_BINARY) 41 | 42 | check: 43 | @cargo fmt 44 | @cargo check 45 | @cargo clippy 46 | 47 | release: 48 | @$(RM) -f Cargo.lock 49 | @$(MAKE) -s 50 | @zip -r "glibc-x86_64-$(VERSION).zip" ./target/release/swhkd ./target/release/swhks 51 | 52 | test: 53 | @cargo test 54 | 55 | clean: 56 | @cargo clean 57 | @$(RM) -f ./docs/*.gz 58 | @$(RM) -f $(DAEMON_BINARY)rc 59 | 60 | setup: 61 | @rustup install stable 62 | @rustup default stable 63 | 64 | .PHONY: check clean setup all install build release 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | SWHKD 3 | 4 |

A next-generation hotkey daemon for Wayland/X11 written in Rust.

5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 |

13 |

14 | 15 | ## SWHKD 16 | 17 | **S**imple **W**ayland **H**ot**K**ey **D**aemon 18 | 19 | `swhkd` is a display protocol-independent hotkey daemon made in 20 | [Rust](https://www.rust-lang.org). `swhkd` uses an easy-to-use configuration 21 | system inspired by `sxhkd`, so you can easily add or remove hotkeys. 22 | 23 | It also attempts to be a drop-in replacement for `sxhkd`, meaning your `sxhkd` 24 | config file is also compatible with `swhkd`. 25 | 26 | Because `swhkd` can be used anywhere, the same `swhkd` config can be used across 27 | Xorg or Wayland desktops, and you can even use `swhkd` in a TTY. 28 | 29 | ## Installation and Building 30 | 31 | [Installation and building instructions can be found here.](./INSTALL.md) 32 | 33 | ## Running 34 | 35 | ```bash 36 | ./swhks && doas ./swhkd 37 | ``` 38 | 39 | The doas or sudo can be skipped by making the swhkd binary a setuid binary. 40 | This can be done by running the following command: 41 | 42 | ```bash 43 | sudo chown root:root swhkd 44 | sudo chmod u+s swhkd 45 | ``` 46 | 47 | then to start, 48 | ```bash 49 | swhks & 50 | swhkd 51 | ``` 52 | 53 | ## Runtime signals 54 | 55 | After opening `swhkd`, you can control the program through signals: 56 | 57 | - `sudo pkill -USR1 swhkd` — Pause key checking 58 | - `sudo pkill -USR2 swhkd` — Resume key checking 59 | - `sudo pkill -HUP swhkd` — Reload config file 60 | 61 | ## Configuration 62 | 63 | `swhkd` closely follows `sxhkd` syntax, so most existing `sxhkd` configs should 64 | be functional with `swhkd`. 65 | More information about the sxhkd syntax can be found in the official man pages from the [arch wiki](https://man.archlinux.org/man/sxhkd.1). 66 | 67 | The default configuration file is in `~/.config/swhkd/swhkdrc` with a fallback to `etc/swhkd/swhkdrc`. 68 | 69 | If you use Vim, you can get `swhkd` config syntax highlighting with the 70 | [swhkd-vim](https://github.com/waycrate/swhkd-vim) plugin. Install it in 71 | vim-plug with `Plug 'waycrate/swhkd-vim'`. 72 | 73 | All supported key and modifier names are listed in `man 5 swhkd-keys`. 74 | 75 | ## Environment Variables 76 | 77 | The environment variables are now sourced using the SWHKS binary, running in the background which are then supplemented 78 | to the command that is to be run, thus emulating the environment variables in the default shell. 79 | 80 | The commands are executed via *SHELL -c 'command'*, hence the environment is sourced from the default shell. 81 | If the user wants to use a different set of environment variables, they can set the environment variables 82 | in the default shell or export the environment variables within a logged in instance of their shell before 83 | running the SWHKS binary. 84 | 85 | ## Autostart 86 | 87 | ### To autostart `swhkd` you can do one of two things 88 | 89 | 1. Add the commands from the ["Running" 90 | section](https://github.com/waycrate/swhkd#running) to your window managers 91 | configuration file. 92 | 1. Enable the [service 93 | file](https://github.com/waycrate/swhkd/tree/main/contrib/init) for your 94 | respective init system. Currently, only systemd and OpenRC service files 95 | exist and more will be added soon including Runit. 96 | 97 | ## Security 98 | 99 | We use a server-client model to keep you safe. The daemon (`swhkd` — privileged 100 | process) is responsible for listening to key events and running shell commands. 101 | The server (`swhks` — non-privileged process) is responsible for keeping a track of the 102 | environment variables and sending them to the daemon. The daemon 103 | uses these environment variables while running the shell commands. 104 | The daemon only runs shell commands that have been parsed from the config file and there is no way to 105 | run arbitrary shell commands. The server is responsible for only sending the environment variables to the daemon and nothing else. 106 | This separation of responsibilities ensures security. 107 | 108 | So yes, you're safe! 109 | 110 | ## Support 111 | 112 | 1. https://matrix.to/#/#waycrate-tools:matrix.org 113 | 1. https://discord.gg/KKZRDYrRYW 114 | 115 | ## Contributors 116 | 117 | 118 | 119 | 120 | 121 | ## Supporters: 122 | 123 | 1. [@CluelessTechnologist](https://github.com/CluelessTechnologist) 124 | -------------------------------------------------------------------------------- /assets/swhkd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waycrate/swhkd/c5c4071459a6465a3743a8bb5bb990e27cdf315b/assets/swhkd.png -------------------------------------------------------------------------------- /contrib/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Aakash Sharma 2 | # Contributor: Sergey A. 3 | # Contributor: rv178 4 | 5 | _pkgname="swhkd" 6 | pkgname="${_pkgname}-git" 7 | pkgver=1.2.1.r92.ge972f55 8 | pkgrel=1 9 | arch=("x86_64") 10 | url="https://github.com/waycrate/swhkd" 11 | pkgdesc="A display server independent hotkey daemon inspired by sxhkd." 12 | license=("BSD") 13 | depends=("polkit") 14 | makedepends=("rustup" "make" "git" "scdoc") 15 | conflicts=("swhkd-musl-git") 16 | source=("${_pkgname}::git+${url}.git" 17 | "${_pkgname}-vim::git+${url}-vim.git") 18 | sha256sums=("SKIP" 19 | "SKIP") 20 | 21 | build(){ 22 | cd "$_pkgname" 23 | make setup 24 | make NO_RFKILL_SW_SUPPORT=1 25 | } 26 | 27 | package() { 28 | cd "$_pkgname" 29 | make DESTDIR="$pkgdir/" install 30 | 31 | cd "${srcdir}/${_pkgname}-vim" 32 | for i in ftdetect ftplugin indent syntax; do 33 | install -Dm644 "$i/${_pkgname}.vim" \ 34 | "${pkgdir}/usr/share/vim/vimfiles/$i/${_pkgname}.vim" 35 | done 36 | } 37 | 38 | pkgver() { 39 | cd $_pkgname 40 | git describe --long --tags --match'=[0-9]*' | sed 's/\([^-]*-g\)/r\1/;s/-/./g' 41 | } 42 | -------------------------------------------------------------------------------- /contrib/init/openrc/README.md: -------------------------------------------------------------------------------- 1 | ## OpenRC Instructions 2 | 3 | To have OpenRC automatically start `swhkd` for you: 4 | 5 | 1. `chmod +x swhkd` 6 | 2. Copy `swhkd` into /etc/init.d/ 7 | 3. Run `sudo rc-update add swhkd` 8 | 4. Run `swhks` on login ( Add it to your `.xinitrc` file or your setup script ) 9 | 10 | -------------------------------------------------------------------------------- /contrib/init/openrc/swhkd: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | command="/usr/bin/swhkd" 4 | command_background=true 5 | pidfile="/run/${RC_SVCNAME}.pid" 6 | 7 | -------------------------------------------------------------------------------- /contrib/init/systemd/README.md: -------------------------------------------------------------------------------- 1 | ## systemd Instructions 2 | 3 | To have systemd automatically start `swhkd` for you: 4 | 5 | 1. Copy `hotkeys.sh` into your preferred directory 6 | 2. `chmod +x hotkeys.sh` 7 | 3. Copy `hotkeys.service` into your `$XDG_CONFIG_DIRS/systemd/user` directory 8 | 4. Using a text editor, uncomment line 7 of `hotkeys.service` and change the path accordingly 9 | 5. In a terminal: `systemctl --user enable hotkeys.service` 10 | -------------------------------------------------------------------------------- /contrib/init/systemd/hotkeys.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=swhkd hotkey daemon 3 | BindsTo=default.target 4 | 5 | [Service] 6 | Type=simple 7 | # ExecStart=/path/to/hotkeys.sh 8 | 9 | [Install] 10 | WantedBy=default.target 11 | -------------------------------------------------------------------------------- /contrib/init/systemd/hotkeys.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | killall swhks 4 | 5 | swhks & pkexec swhkd 6 | -------------------------------------------------------------------------------- /docs/swhkd-keys.5.scd: -------------------------------------------------------------------------------- 1 | swhkd(5) "github.com/waycrate/swhkd" "File Formats Manual" 2 | 3 | # NAME 4 | 5 | swhkd - Hotkey daemon inspired by sxhkd written in Rust 6 | 7 | # VALID MODIFIERS 8 | 9 | - Ctrl 10 | - Control 11 | - Super 12 | - Mod4 13 | - Alt 14 | - Mod1 15 | - Altgr 16 | - Mod5 17 | - Shift 18 | 19 | # VALID KEYS 20 | - q 21 | - w 22 | - e 23 | - r 24 | - t 25 | - y 26 | - u 27 | - i 28 | - o 29 | - p 30 | - a 31 | - s 32 | - d 33 | - f 34 | - g 35 | - h 36 | - j 37 | - k 38 | - l 39 | - z 40 | - x 41 | - c 42 | - v 43 | - b 44 | - n 45 | - m 46 | - 1 47 | - 2 48 | - 3 49 | - 4 50 | - 5 51 | - 6 52 | - 7 53 | - 8 54 | - 9 55 | - 0 56 | - escape 57 | - backspace 58 | - capslock 59 | - return 60 | - enter 61 | - tab 62 | - space 63 | - plus 64 | - kp0 65 | - kp1 66 | - kp2 67 | - kp3 68 | - kp4 69 | - kp5 70 | - kp6 71 | - kp7 72 | - kp8 73 | - kp9 74 | - kpasterisk 75 | - kpcomma 76 | - kpdot 77 | - kpenter 78 | - kpequal 79 | - kpjpcomma 80 | - kpleftparen 81 | - kpminus 82 | - kpplusminus 83 | - kprightparen 84 | - minus 85 | - - 86 | - equal 87 | - = 88 | - grave 89 | - ` 90 | - print 91 | - volumeup 92 | - xf86audioraisevolume 93 | - volumedown 94 | - xf86audiolowervolume 95 | - mute 96 | - xf86audiomute 97 | - brightnessup 98 | - xf86monbrightnessup 99 | - brightnessdown 100 | - xf86audiomedia 101 | - xf86audiomicmute 102 | - micmute 103 | - xf86audionext 104 | - xf86audioplay 105 | - xf86audioprev 106 | - xf86audiostop 107 | - xf86monbrightnessdown 108 | - , 109 | - comma 110 | - . 111 | - dot 112 | - period 113 | - / 114 | - question 115 | - slash 116 | - backslash 117 | - leftbrace 118 | - [ 119 | - bracketleft 120 | - rightbrace 121 | - ] 122 | - bracketright 123 | - ; 124 | - scroll_lock 125 | - semicolon 126 | - ' 127 | - apostrophe 128 | - left 129 | - right 130 | - up 131 | - down 132 | - pause 133 | - home 134 | - delete 135 | - insert 136 | - end 137 | - pause 138 | - prior 139 | - next 140 | - pagedown 141 | - pageup 142 | - f1 143 | - f2 144 | - f3 145 | - f4 146 | - f5 147 | - f6 148 | - f7 149 | - f8 150 | - f9 151 | - f10 152 | - f11 153 | - f12 154 | - f13 155 | - f14 156 | - f15 157 | - f16 158 | - f17 159 | - f18 160 | - f19 161 | - f20 162 | - f21 163 | - f22 164 | - f23 165 | - f24 166 | 167 | # AUTHORS 168 | 169 | Maintained by Shinyzenith , EdenQwQ , and Angelo Fallaria . 170 | For more information about development, see . 171 | 172 | # SEE ALSO 173 | 174 | - *swhkd(1)* 175 | - *swhkd(5)* 176 | - *swhks(1)* 177 | -------------------------------------------------------------------------------- /docs/swhkd.1.scd: -------------------------------------------------------------------------------- 1 | swhkd(1) "github.com/shinyzenith/swhkd" "General Commands Manual" 2 | 3 | # NAME 4 | 5 | swhkd - Hotkey daemon inspired by sxhkd written in Rust 6 | 7 | # SYNOPSIS 8 | 9 | *swhkd* [_flags_] 10 | 11 | # CONFIG FILE 12 | 13 | The config file goes in *~/.config/swhkd/swhkdrc* with a fallback to */etc/swhkd/swhkdrc*. 14 | More about the config file syntax in `swhkd(5)` 15 | 16 | # OPTIONS 17 | 18 | *-h*, *--help* 19 | Print help message and quit. 20 | 21 | *-V*, *--version* 22 | Print version information. 23 | 24 | *-c*, *--config* 25 | Set a custom config file path. 26 | 27 | *-C*, *--cooldown* 28 | Set a custom repeat cooldown duration. Default is 250ms. Most wayland 29 | compositors handle this server side however, either way works. 30 | 31 | *-d*, *--debug* 32 | Enable debug mode. 33 | 34 | *-D, --device* 35 | Manually set the keyboard devices to use. Can occur multiple times. 36 | 37 | # SIGNALS 38 | 39 | - Reload config file: `sudo pkill -HUP swhkd` 40 | - Pause Hotkey checking: `sudo pkill -USR1 swhkd` 41 | - Resume key checking: `sudo pkill -USR2 swhkd` 42 | 43 | # AUTHORS 44 | 45 | Maintained by Shinyzenith , EdenQwQ , and Angelo Fallaria . 46 | For more information about development, see . 47 | 48 | # SEE ALSO 49 | 50 | - *swhkd(5)* 51 | - *swhkd-keys(5)* 52 | - *swhks(1)* 53 | -------------------------------------------------------------------------------- /docs/swhkd.5.scd: -------------------------------------------------------------------------------- 1 | swhkd(5) "github.com/waycrate/swhkd" "File Formats Manual" 2 | 3 | # NAME 4 | 5 | swhkd - Hotkey daemon inspired by sxhkd written in Rust 6 | 7 | # CONFIG FILE 8 | 9 | - A global config can be defined in *~/.config/swhkd/swhkdrc*, with a 10 | fallback to */etc/swhkd/swhkdrc*. Swhkd attempts to look in your *$XDG_CONFIG_HOME*, failing which it defaults to *~/.config*. 11 | - A local config overrides the global one. Local configs should be placed in the root of the project. 12 | - The config file can also be specified with the *-c* flag. 13 | 14 | # ENVIRONMENT 15 | 16 | - The environment variables are now sourced using the SWHKS binary, running in the background. 17 | - The environment variables are then supplemented to the command that is to be run, thus emulating the 18 | environment variables in the default shell. 19 | - The commands are executed via *SHELL -c 'command'*, hence the environment is sourced from the default shell. 20 | - If the user wants to use a different set of environment variables, they can set the environment variables 21 | in the default shell or export the environment variables within a logged in instance of their shell before 22 | running the SWHKS binary. 23 | 24 | # SYNTAX 25 | 26 | The syntax of the configuration file is identical to sxhkd and builds upon it. 27 | More information about the syntax can be found from the official sxhkd documentation: 28 | https://man.archlinux.org/man/sxhkd.1, however a brief summary of it is provided below. 29 | 30 | Each line of the configuration file is interpreted as so: 31 | - If it is empty or starts with #, it is ignored. 32 | - If it starts with a space, it is read as a command. 33 | - Otherwise, it is read as a hotkey. 34 | 35 | For valid keys and modifiers, check *swhkd-keys(5)* 36 | 37 | # EXAMPLE 38 | 39 | ``` 40 | # Import another configuration file. 41 | # NOTE: the path provided must be absolute and not relative such as `~`. 42 | include /home/YourUserName/.config/swhkd/swhkdrc 43 | 44 | ignore alt + print # globally ignore a key binding 45 | 46 | # terminal 47 | super + ReTuRn # case insensitive 48 | alacritty 49 | 50 | super + shift + enter # enter = return 51 | kitty 52 | 53 | # file manager 54 | super + shift + f 55 | pcmanfm 56 | 57 | # web-browser 58 | super + w 59 | firefox 60 | 61 | # bspwm 62 | super + {_,shift + }{h,j,k,l} 63 | bspc node -{f,s} {west,south,north,east} 64 | 65 | super + ctrl + alt + {Left\ 66 | ,Down\ 67 | ,Up\ 68 | ,Right} 69 | n=10; \ 70 | { d1=left; d2=right; dx=-$n; dy=0; \ 71 | , d1=bottom; d2=top; dx=0; dy=$n; \ 72 | , d1=top; d2=bottom; dx=0; dy=-$n; \ 73 | , d1=right; d2=left; dx=$n; dy=0; \ 74 | } \ 75 | bspc node --resize $d1 $dx $dy || bspc node --resize $d2 $dx $dy 76 | 77 | super + {\,, .} 78 | bspc node -f {next.local,prev.local} 79 | 80 | # screenshot 81 | print 82 | scrot 83 | 84 | any + print # any represent at least one of the valid modifiers 85 | scrot -s 86 | 87 | # Append with @ to run on key-release. 88 | @super + shift + f 89 | pcmanfm 90 | 91 | # Append with ~ to emit the hotkey after the command is triggered. Aka, don't swallow the hotkey. 92 | ~super + shift + f 93 | pcmanfm 94 | 95 | super + m 96 | # commands starting with @ are internal commands. 97 | # internal commands can be combined with normal commands with '&&'. 98 | # '@enter' pushes a mode into the mode stack and starts listening only the 99 | # key bindings defined in that mode 100 | @enter music && echo "music" > ~/.config/waybar/swhkd-mode 101 | 102 | mode music # use the mode statement to define a mode 103 | q 104 | # '@escape' pops the current mode out of the mode stack 105 | # the default mode is 'normal mode', which is always on the bottom of the mode 106 | # stack and can never be escaped 107 | @escape && echo "normal" > ~/.config/waybar/swhkd-mode 108 | {n, p, space, r, z, y} 109 | mpc {next, prev, toggle, repeat, random, single} 110 | endmode # use endmode if you want to set more key bindings for normal mode 111 | 112 | # mode options are declared after the mode name 113 | # swallow: don't emit any event through uinput 114 | # oneoff: automatically escape a mode when a keybinding defined in it is evoked 115 | mode option_demo swallow oneoff 116 | a 117 | echo 0 118 | b 119 | @escape # escaping in a 'oneoff' mode pops two modes out of the mode stack. 120 | endmode 121 | 122 | ``` 123 | # AUTHORS 124 | 125 | Maintained by Shinyzenith , EdenQwQ , and Angelo Fallaria . 126 | For more information about development, see . 127 | 128 | # SEE ALSO 129 | 130 | - *swhkd(1)* 131 | - *swhkd-keys(5)* 132 | - *swhks(1)* 133 | -------------------------------------------------------------------------------- /docs/swhks.1.scd: -------------------------------------------------------------------------------- 1 | swhks(1) "github.com/shinyzenith/swhkd" "General Commands Manual" 2 | 3 | # NAME 4 | 5 | swhks - Server for swhkd, used to source environment variables from the default shell to supply to swhkd. 6 | 7 | # SYNOPSIS 8 | 9 | *swhks* 10 | 11 | # OPTIONS 12 | 13 | *-h*, *--help* 14 | Print help message and quit. 15 | 16 | *-V*, *--version* 17 | Print version information. 18 | 19 | *-d*, *--debug* 20 | Enable debug mode. 21 | 22 | # AUTHORS 23 | 24 | Maintained by Shinyzenith , EdenQwQ , and Angelo Fallaria . 25 | For more information about development, see . 26 | 27 | # SEE ALSO 28 | 29 | - *swhkd(1)* 30 | - *swhkd(5)* 31 | - *swhkd-keys(5)* 32 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1715499532, 6 | "narHash": "sha256-9UJLb8rdi2VokYcfOBQHUzP3iNxOPNWcbK++ENElpk0=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "af8b9db5c00f1a8e4b83578acc578ff7d823b786", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Swhkd devel"; 3 | 4 | inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; }; 5 | 6 | outputs = { self, nixpkgs, ... }: 7 | let 8 | pkgsFor = system: 9 | import nixpkgs { 10 | inherit system; 11 | overlays = [ ]; 12 | }; 13 | 14 | targetSystems = [ "aarch64-linux" "x86_64-linux" ]; 15 | in { 16 | devShells = nixpkgs.lib.genAttrs targetSystems (system: 17 | let pkgs = pkgsFor system; 18 | in { 19 | default = pkgs.mkShell { 20 | name = "Swhkd-devel"; 21 | nativeBuildInputs = with pkgs; [ 22 | # Compilers 23 | cargo 24 | rustc 25 | scdoc 26 | 27 | # libs 28 | udev 29 | 30 | # Tools 31 | pkg-config 32 | clippy 33 | gdb 34 | gnumake 35 | rust-analyzer 36 | rustfmt 37 | strace 38 | valgrind 39 | zip 40 | ]; 41 | }; 42 | }); 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /scripts/build-polkit-policy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ############### 4 | # Defaults 5 | ############### 6 | 7 | readonly DEFAULT_SWHKD_PATH="/usr/bin/swhkd" 8 | readonly DEFAULT_POLICY_PATH="com.github.swhkd.pkexec.policy" 9 | readonly DEFAULT_MESSAGE="Authentication is required to run Simple Wayland Hotkey Daemon" 10 | readonly DEFAULT_ACTION_ID="com.github.swhkd.pkexec" 11 | 12 | ############### 13 | # Init 14 | ############### 15 | 16 | print_help() { 17 | printf "Usage: build-polkit-policy [OPTIONS]\n\n" 18 | printf "Generates a polkit policy file for swhkd.\n\n" 19 | printf "Optional Arguments:\n" 20 | printf " --policy-path= Path to save the policy file to.\n" 21 | printf " If set to '-', this tool will output to stdout instead.\n" 22 | printf " Defaults to '%s'.\n" "${DEFAULT_POLICY_PATH}" 23 | printf " --swhkd-path= Path to the swhkd binary when installed.\n" 24 | printf " Defaults to '%s'.\n" "${DEFAULT_SWHKD_PATH}" 25 | printf " --action-id= Polkit action id to use.\n" 26 | printf " Defaults to '%s'.\n" "${DEFAULT_ACTION_ID}" 27 | printf " --message= Custom authentication message.\n" 28 | printf " Defaults to '%s'\n" "${DEFAULT_MESSAGE}" 29 | printf " -h|--help Show this help.\n" 30 | } 31 | 32 | while [ -n "$1" ]; do 33 | case "$1" in 34 | --policy-path=*) 35 | POLICY_PATH=${1#*=} 36 | shift 37 | ;; 38 | --swhkd-path=*) 39 | SWHKD_PATH=${1#*=} 40 | shift 41 | ;; 42 | --action-id=*) 43 | ACTION_ID=${1#*=} 44 | shift 45 | ;; 46 | --message=*) 47 | MESSAGE=${1#*=} 48 | shift 49 | ;; 50 | -h|--help) 51 | print_help 52 | exit 0 53 | ;; 54 | *) 55 | printf "Unknown option '%s'. Aborting.\n" "$1" 56 | exit 1 57 | ;; 58 | esac 59 | done 60 | 61 | print_policy() { 62 | cat << EOF 63 | 64 | 65 | 66 | 67 | ${MESSAGE} 68 | 69 | no 70 | no 71 | yes 72 | 73 | ${SWHKD_PATH} 74 | 75 | 76 | EOF 77 | } 78 | 79 | # No local variables in POSIX sh, so just set these globally 80 | POLICY_PATH="${POLICY_PATH:-${DEFAULT_POLICY_PATH}}" 81 | SWHKD_PATH="${SWHKD_PATH:-${DEFAULT_SWHKD_PATH}}" 82 | ACTION_ID="${ACTION_ID:-${DEFAULT_ACTION_ID}}" 83 | MESSAGE="${MESSAGE:-${DEFAULT_MESSAGE}}" 84 | 85 | if [ "${POLICY_PATH}" = "-" ]; then 86 | print_policy 87 | else 88 | print_policy > "${POLICY_PATH}" 89 | fi 90 | -------------------------------------------------------------------------------- /swhkd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Sxhkd clone for Wayland (works on TTY and X11 too)" 3 | edition = "2021" 4 | name = "Simple-Wayland-HotKey-Daemon" 5 | version = "1.3.0-dev" 6 | authors = [ 7 | "Shinyzenith \n", 8 | "Angelo Fallaria \n", 9 | "EdenQwQ \n", 10 | ] 11 | 12 | [build-dependencies] 13 | flate2 = "1.0.24" 14 | 15 | [features] 16 | no_rfkill = [] 17 | 18 | [dependencies] 19 | clap = { version = "4.1.0", features = ["derive"] } 20 | env_logger = "0.9.0" 21 | evdev = { version = "0.12.0", features = ["tokio"] } 22 | itertools = "0.10.3" 23 | log = "0.4.14" 24 | nix = "0.23.1" 25 | signal-hook = "0.3.13" 26 | signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } 27 | sweet = { git = "https://github.com/waycrate/sweet.git", version = "0.4.0" } 28 | sysinfo = "0.23.5" 29 | tokio = { version = "1.24.2", features = ["full"] } 30 | tokio-stream = "0.1.8" 31 | tokio-udev = "0.9.1" 32 | 33 | [[bin]] 34 | name = "swhkd" 35 | path = "src/daemon.rs" 36 | -------------------------------------------------------------------------------- /swhkd/build.rs: -------------------------------------------------------------------------------- 1 | extern crate flate2; 2 | use flate2::{write::GzEncoder, Compression}; 3 | use std::{ 4 | fs::{read_dir, File, OpenOptions}, 5 | io::{copy, BufReader, ErrorKind}, 6 | path::Path, 7 | process::{exit, Command, Stdio}, 8 | }; 9 | 10 | fn main() { 11 | if let Err(e) = Command::new("scdoc") 12 | .stdin(Stdio::null()) 13 | .stdout(Stdio::null()) 14 | .stderr(Stdio::null()) 15 | .spawn() 16 | { 17 | if let ErrorKind::NotFound = e.kind() { 18 | exit(0); 19 | } 20 | } 21 | 22 | // We just append "out" so it's easy to find all the scdoc output later in line 38. 23 | let man_pages: Vec<(String, String)> = read_and_replace_by_ext("../docs", ".scd", ".out"); 24 | for man_page in man_pages { 25 | let output = 26 | OpenOptions::new().write(true).create(true).open(Path::new(&man_page.1)).unwrap(); 27 | _ = Command::new("scdoc") 28 | .stdin(Stdio::from(File::open(man_page.0).unwrap())) 29 | .stdout(output) 30 | .spawn(); 31 | } 32 | 33 | // Gzipping the man pages 34 | let scdoc_output_files: Vec<(String, String)> = 35 | read_and_replace_by_ext("../docs", ".out", ".gz"); 36 | for scdoc_output in scdoc_output_files { 37 | let mut input = BufReader::new(File::open(scdoc_output.0).unwrap()); 38 | let output = 39 | OpenOptions::new().write(true).create(true).open(Path::new(&scdoc_output.1)).unwrap(); 40 | let mut encoder = GzEncoder::new(output, Compression::default()); 41 | copy(&mut input, &mut encoder).unwrap(); 42 | encoder.finish().unwrap(); 43 | } 44 | } 45 | 46 | fn read_and_replace_by_ext(path: &str, search: &str, replace: &str) -> Vec<(String, String)> { 47 | let mut files: Vec<(String, String)> = Vec::new(); 48 | for path in read_dir(path).unwrap() { 49 | let path = path.unwrap(); 50 | if path.file_type().unwrap().is_dir() { 51 | continue; 52 | } 53 | 54 | if let Some(file_name) = path.path().to_str() { 55 | if *path.path().extension().unwrap().to_str().unwrap() != search[1..] { 56 | continue; 57 | } 58 | 59 | let file = file_name.replace(search, replace); 60 | files.push((file_name.to_string(), file)); 61 | } 62 | } 63 | files 64 | } 65 | -------------------------------------------------------------------------------- /swhkd/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::path::Path; 3 | use sweet::KeyAttribute; 4 | use sweet::{Definition, SwhkdParser}; 5 | use sweet::{ModeInstruction, ParseError}; 6 | 7 | pub fn load(path: &Path) -> Result, ParseError> { 8 | let config_self = sweet::SwhkdParser::from(sweet::ParserInput::Path(path))?; 9 | parse_contents(config_self) 10 | } 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct KeyBinding { 14 | pub keysym: evdev::Key, 15 | pub modifiers: HashSet, 16 | pub send: bool, 17 | pub on_release: bool, 18 | } 19 | 20 | impl PartialEq for KeyBinding { 21 | fn eq(&self, other: &Self) -> bool { 22 | self.keysym == other.keysym 23 | // Comparisons are order independent without manual iterations 24 | && self.modifiers == other.modifiers 25 | && self.send == other.send 26 | && self.on_release == other.on_release 27 | } 28 | } 29 | 30 | pub trait Prefix { 31 | fn send(self) -> Self; 32 | fn on_release(self) -> Self; 33 | } 34 | 35 | pub trait Value { 36 | fn keysym(&self) -> evdev::Key; 37 | fn modifiers(&self) -> &HashSet; 38 | fn is_send(&self) -> bool; 39 | fn is_on_release(&self) -> bool; 40 | } 41 | 42 | impl KeyBinding { 43 | pub fn new(keysym: evdev::Key, modifiers: HashSet) -> Self { 44 | KeyBinding { keysym, modifiers, send: false, on_release: false } 45 | } 46 | 47 | pub fn on_release(mut self) -> Self { 48 | self.on_release = true; 49 | self 50 | } 51 | } 52 | 53 | impl Prefix for KeyBinding { 54 | fn send(mut self) -> Self { 55 | self.send = true; 56 | self 57 | } 58 | fn on_release(mut self) -> Self { 59 | self.on_release = true; 60 | self 61 | } 62 | } 63 | 64 | impl Value for KeyBinding { 65 | fn keysym(&self) -> evdev::Key { 66 | self.keysym 67 | } 68 | fn modifiers(&self) -> &HashSet { 69 | &self.modifiers 70 | } 71 | fn is_send(&self) -> bool { 72 | self.send 73 | } 74 | fn is_on_release(&self) -> bool { 75 | self.on_release 76 | } 77 | } 78 | 79 | #[derive(Debug, Clone, PartialEq)] 80 | pub struct Hotkey { 81 | pub keybinding: KeyBinding, 82 | pub command: String, 83 | pub mode_instructions: Vec, 84 | } 85 | 86 | #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] 87 | pub enum Modifier { 88 | Super, 89 | Alt, 90 | Altgr, 91 | Control, 92 | Shift, 93 | Any, 94 | } 95 | 96 | impl Hotkey { 97 | pub fn from_keybinding(keybinding: KeyBinding, command: String) -> Self { 98 | Hotkey { keybinding, command, mode_instructions: vec![] } 99 | } 100 | 101 | /// Accepts both Vec and HashSet and stored as HashSet 102 | #[cfg(test)] 103 | pub fn new( 104 | keysym: evdev::Key, 105 | modifiers: impl IntoIterator, 106 | command: String, 107 | ) -> Self { 108 | Hotkey { 109 | keybinding: KeyBinding::new(keysym, modifiers.into_iter().collect()), 110 | command, 111 | mode_instructions: vec![], 112 | } 113 | } 114 | } 115 | 116 | impl Prefix for Hotkey { 117 | fn send(mut self) -> Self { 118 | self.keybinding.send = true; 119 | self 120 | } 121 | fn on_release(mut self) -> Self { 122 | self.keybinding.on_release = true; 123 | self 124 | } 125 | } 126 | 127 | impl Value for &Hotkey { 128 | fn keysym(&self) -> evdev::Key { 129 | self.keybinding.keysym 130 | } 131 | fn modifiers(&self) -> &HashSet { 132 | &self.keybinding.modifiers 133 | } 134 | fn is_send(&self) -> bool { 135 | self.keybinding.send 136 | } 137 | fn is_on_release(&self) -> bool { 138 | self.keybinding.on_release 139 | } 140 | } 141 | 142 | #[derive(Debug, Clone, PartialEq)] 143 | pub struct Mode { 144 | pub name: String, 145 | pub hotkeys: Vec, 146 | pub unbinds: Vec, 147 | pub options: ModeOptions, 148 | } 149 | 150 | impl Default for Mode { 151 | fn default() -> Self { 152 | Self { 153 | name: "normal".to_string(), 154 | hotkeys: vec![], 155 | unbinds: vec![], 156 | options: ModeOptions::default(), 157 | } 158 | } 159 | } 160 | 161 | #[derive(Debug, Clone, PartialEq, Default)] 162 | pub struct ModeOptions { 163 | pub swallow: bool, 164 | pub oneoff: bool, 165 | } 166 | 167 | pub fn parse_contents(contents: SwhkdParser) -> Result, ParseError> { 168 | let mut default_mode = Mode::default(); 169 | 170 | for binding in &contents.bindings { 171 | default_mode.hotkeys.push(Hotkey { 172 | keybinding: sweet_def_to_kb(&binding.definition), 173 | command: binding.command.clone(), 174 | mode_instructions: binding.mode_instructions.clone(), 175 | }); 176 | } 177 | default_mode.unbinds.extend(contents.unbinds.iter().map(sweet_def_to_kb)); 178 | 179 | let mut modes = vec![default_mode]; 180 | 181 | for sweet::Mode { name, oneoff, swallow, bindings, unbinds } in contents.modes { 182 | let mut pushmode = 183 | Mode { name, options: ModeOptions { swallow, oneoff }, ..Default::default() }; 184 | 185 | for binding in bindings { 186 | let hotkey = Hotkey { 187 | keybinding: sweet_def_to_kb(&binding.definition), 188 | command: binding.command, 189 | mode_instructions: binding.mode_instructions.clone(), 190 | }; 191 | // Replace existing hotkeys with same keybinding 192 | pushmode.hotkeys.retain(|h| h.keybinding.keysym != hotkey.keybinding.keysym); 193 | pushmode.hotkeys.push(hotkey); 194 | } 195 | pushmode.unbinds.extend(unbinds.iter().map(sweet_def_to_kb)); 196 | 197 | modes.push(pushmode); 198 | } 199 | Ok(modes) 200 | } 201 | 202 | /// Convert sweet::Definition to KeyBinding 203 | fn sweet_def_to_kb(def: &Definition) -> KeyBinding { 204 | let modifiers: HashSet = def 205 | .modifiers 206 | .iter() 207 | .filter_map(|m| match m { 208 | sweet::Modifier::Super => Some(Modifier::Super), 209 | sweet::Modifier::Any => Some(Modifier::Any), 210 | sweet::Modifier::Control => Some(Modifier::Control), 211 | sweet::Modifier::Alt => Some(Modifier::Alt), 212 | sweet::Modifier::Altgr => Some(Modifier::Altgr), 213 | sweet::Modifier::Shift => Some(Modifier::Shift), 214 | sweet::Modifier::Omission => None, 215 | }) 216 | .collect(); 217 | 218 | KeyBinding { 219 | keysym: def.key.key, 220 | modifiers, 221 | send: def.key.attribute == KeyAttribute::Send, 222 | on_release: def.key.attribute == KeyAttribute::OnRelease, 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /swhkd/src/daemon.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Value; 2 | use clap::Parser; 3 | use config::Hotkey; 4 | use evdev::{AttributeSet, Device, InputEventKind, Key}; 5 | use nix::{ 6 | sys::stat::{umask, Mode}, 7 | unistd::{setgid, setuid, Gid, Uid}, 8 | }; 9 | use signal_hook::consts::signal::*; 10 | use signal_hook_tokio::Signals; 11 | use std::{ 12 | collections::{HashMap, HashSet}, 13 | env, 14 | error::Error, 15 | fs::{self, File, OpenOptions, Permissions}, 16 | io::{Read, Write}, 17 | os::unix::{fs::PermissionsExt, net::UnixStream}, 18 | path::{Path, PathBuf}, 19 | process::{exit, id, Command, Stdio}, 20 | sync::{Arc, Mutex}, 21 | time::{SystemTime, UNIX_EPOCH}, 22 | }; 23 | use sysinfo::{ProcessExt, System, SystemExt}; 24 | use tokio::time::Duration; 25 | use tokio::time::{sleep, Instant}; 26 | use tokio::{select, sync::mpsc}; 27 | use tokio_stream::{StreamExt, StreamMap}; 28 | use tokio_udev::{AsyncMonitorSocket, EventType, MonitorBuilder}; 29 | 30 | mod config; 31 | mod environ; 32 | mod perms; 33 | mod uinput; 34 | 35 | struct KeyboardState { 36 | state_modifiers: HashSet, 37 | state_keysyms: AttributeSet, 38 | } 39 | 40 | impl KeyboardState { 41 | fn new() -> KeyboardState { 42 | KeyboardState { state_modifiers: HashSet::new(), state_keysyms: AttributeSet::new() } 43 | } 44 | } 45 | 46 | /// Simple Wayland Hotkey Daemon 47 | #[derive(Parser)] 48 | #[command(version, about, long_about = None)] 49 | struct Args { 50 | /// Set a custom config file path. 51 | #[arg(short = 'c', long, value_name = "FILE")] 52 | config: Option, 53 | 54 | /// Set a custom repeat cooldown duration. Default is 250ms. 55 | #[arg(short = 'C', long, default_value_t = 250)] 56 | cooldown: u64, 57 | 58 | /// Enable Debug Mode 59 | #[arg(short, long)] 60 | debug: bool, 61 | 62 | /// Take a list of devices from the user 63 | #[arg(short = 'D', long, num_args = 0.., value_delimiter = ' ')] 64 | device: Vec, 65 | 66 | /// Set a custom log file. (Defaults to ${XDG_DATA_HOME:-$HOME/.local/share}/swhks-current_unix_time.log) 67 | #[arg(short, long, value_name = "FILE")] 68 | log: Option, 69 | } 70 | 71 | #[tokio::main] 72 | async fn main() -> Result<(), Box> { 73 | let args = Args::parse(); 74 | env::set_var("RUST_LOG", "swhkd=warn"); 75 | 76 | if args.debug { 77 | env::set_var("RUST_LOG", "swhkd=trace"); 78 | } 79 | 80 | env_logger::init(); 81 | log::trace!("Logger initialized."); 82 | 83 | // Just to double check that we are in root 84 | perms::raise_privileges(); 85 | 86 | // Get the UID of the user that is not a system user 87 | let invoking_uid = get_uid()?; 88 | 89 | log::debug!("Wating for server to start..."); 90 | // The first and the most important request for the env 91 | // Without this request, the environmental variables responsible for the reading for the config 92 | // file will not be available. 93 | // Thus, it is important to wait for the server to start before proceeding. 94 | let env; 95 | let mut env_hash; 96 | loop { 97 | match refresh_env(invoking_uid, 0) { 98 | Ok((Some(new_env), hash)) => { 99 | env_hash = hash; 100 | env = new_env; 101 | break; 102 | } 103 | Ok((None, _)) => { 104 | log::debug!("Waiting for env..."); 105 | continue; 106 | } 107 | Err(_) => {} 108 | } 109 | } 110 | log::trace!("Environment Aquired"); 111 | 112 | // Now that we have the env, we can safely proceed with the rest of the program. 113 | // Log handling 114 | let log_file_name = if let Some(val) = args.log { 115 | val 116 | } else { 117 | let time = match SystemTime::now().duration_since(UNIX_EPOCH) { 118 | Ok(n) => n.as_secs().to_string(), 119 | Err(_) => { 120 | log::error!("SystemTime before UnixEpoch!"); 121 | exit(1); 122 | } 123 | }; 124 | 125 | format!("{}/swhkd/swhkd-{}.log", env.fetch_xdg_data_path().to_string_lossy(), time).into() 126 | }; 127 | 128 | let log_path = PathBuf::from(&log_file_name); 129 | if let Some(p) = log_path.parent() { 130 | if !p.exists() { 131 | if let Err(e) = fs::create_dir_all(p) { 132 | log::error!("Failed to create log dir: {}", e); 133 | } 134 | } 135 | } 136 | // if file doesnt exist, create it with 0666 permissions 137 | if !log_path.exists() { 138 | if let Err(e) = OpenOptions::new().append(true).create(true).open(&log_path) { 139 | log::error!("Failed to create log file: {}", e); 140 | exit(1); 141 | } 142 | fs::set_permissions(&log_path, Permissions::from_mode(0o666)).unwrap(); 143 | } 144 | 145 | // Calculate a server cooldown at which the server will be pinged to check for env changes. 146 | let cooldown = args.cooldown; 147 | let delta = (cooldown as f64 * 0.1) as u64; 148 | let server_cooldown = std::cmp::max(0, cooldown - delta); 149 | 150 | // Set up a channel to communicate with the server 151 | // The channel can have upto 100 commands in the queue 152 | let (tx, mut rx) = tokio::sync::mpsc::channel::(100); 153 | 154 | // We use a arc mutex to make sure that our pairs are valid and also concurrent 155 | // while being used by the threads. 156 | let pairs = Arc::new(Mutex::new(env.pairs.clone())); 157 | let pairs_clone = Arc::clone(&pairs); 158 | let log = log_path.clone(); 159 | 160 | // We spawn a new thread in the user space to act as the execution thread 161 | // This again has a thread for running the env refresh module when a change is detected from 162 | // the server. 163 | tokio::spawn(async move { 164 | // This is the thread that is responsible for refreshing the env 165 | // It's sleep time is determined by the server cooldown. 166 | tokio::spawn(async move { 167 | loop { 168 | { 169 | let mut pairs = pairs_clone.lock().unwrap(); 170 | match refresh_env(invoking_uid, env_hash) { 171 | Ok((Some(env), hash)) => { 172 | pairs.clone_from(&env.pairs); 173 | env_hash = hash; 174 | } 175 | Ok((None, hash)) => { 176 | env_hash = hash; 177 | } 178 | Err(e) => { 179 | log::error!("Error: {}", e); 180 | _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); 181 | exit(1); 182 | } 183 | } 184 | } 185 | sleep(Duration::from_millis(server_cooldown)).await; 186 | } 187 | }); 188 | 189 | // When we do receive a command, we spawn a new thread to execute the command 190 | // This thread is spawned in the user space and is used to execute the command and it 191 | // exits after the command is executed. 192 | while let Some(command) = rx.recv().await { 193 | // Clone the arc references to be used in the thread 194 | let pairs = pairs.clone(); 195 | let log = log.clone(); 196 | 197 | // Set the user and group id to the invoking user for the thread 198 | setgid(Gid::from_raw(invoking_uid)).unwrap(); 199 | setuid(Uid::from_raw(invoking_uid)).unwrap(); 200 | 201 | // Command execution 202 | let mut cmd = Command::new("sh"); 203 | cmd.arg("-c") 204 | .arg(command) 205 | .stdin(Stdio::null()) 206 | .stdout(match File::open(&log) { 207 | Ok(file) => file, 208 | Err(e) => { 209 | println!("Error: {}", e); 210 | _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); 211 | exit(1); 212 | } 213 | }) 214 | .stderr(match File::open(&log) { 215 | Ok(file) => file, 216 | Err(e) => { 217 | println!("Error: {}", e); 218 | _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); 219 | exit(1); 220 | } 221 | }); 222 | 223 | // Set the environment variables for the command 224 | for (key, value) in pairs.lock().unwrap().iter() { 225 | cmd.env(key, value); 226 | } 227 | 228 | match cmd.spawn() { 229 | Ok(_) => { 230 | log::info!("Command executed successfully."); 231 | } 232 | Err(e) => log::error!("Failed to execute command: {}", e), 233 | } 234 | } 235 | }); 236 | 237 | // With the threads responsible for refresh and execution being in place, we can finally 238 | // start the main loop of the program. 239 | setup_swhkd(invoking_uid, env.xdg_runtime_dir(invoking_uid)); 240 | 241 | let config_file_path: PathBuf = 242 | args.config.as_ref().map_or_else(|| env.fetch_xdg_config_path(), |file| file.clone()); 243 | let load_config = || { 244 | log::debug!("Using config file path: {:#?}", config_file_path); 245 | 246 | match config::load(&config_file_path) { 247 | Err(e) => { 248 | log::error!("Config Error: {}", e); 249 | if let Some(error_source) = e.source() { 250 | log::error!("{}", error_source); 251 | } 252 | exit(1) 253 | } 254 | Ok(out) => out, 255 | } 256 | }; 257 | 258 | let mut modes = load_config(); 259 | let mut mode_stack: Vec = vec![0]; 260 | let arg_devices: Vec = args.device; 261 | 262 | let keyboard_devices: Vec<_> = { 263 | if arg_devices.is_empty() { 264 | log::trace!("Attempting to find all keyboard file descriptors."); 265 | evdev::enumerate().filter(|(_, dev)| check_device_is_keyboard(dev)).collect() 266 | } else { 267 | evdev::enumerate() 268 | .filter(|(_, dev)| arg_devices.contains(&dev.name().unwrap_or("").to_string())) 269 | .collect() 270 | } 271 | }; 272 | 273 | if keyboard_devices.is_empty() { 274 | log::error!("No valid keyboard device was detected!"); 275 | exit(1); 276 | } 277 | 278 | log::debug!("{} Keyboard device(s) detected.", keyboard_devices.len()); 279 | 280 | // Apparently, having a single uinput device with keys, relative axes and switches 281 | // prevents some libraries to listen to these events. The easy fix is to have separate 282 | // virtual devices, one for keys and relative axes (`uinput_device`) and another one 283 | // just for switches (`uinput_switches_device`). 284 | let mut uinput_device = match uinput::create_uinput_device() { 285 | Ok(dev) => dev, 286 | Err(e) => { 287 | log::error!("Failed to create uinput device: \nErr: {:#?}", e); 288 | exit(1); 289 | } 290 | }; 291 | 292 | let mut uinput_switches_device = match uinput::create_uinput_switches_device() { 293 | Ok(dev) => dev, 294 | Err(e) => { 295 | log::error!("Failed to create uinput switches device: \nErr: {:#?}", e); 296 | exit(1); 297 | } 298 | }; 299 | 300 | let mut udev = 301 | AsyncMonitorSocket::new(MonitorBuilder::new()?.match_subsystem("input")?.listen()?)?; 302 | 303 | let modifiers_map: HashMap = HashMap::from([ 304 | (Key::KEY_LEFTMETA, config::Modifier::Super), 305 | (Key::KEY_RIGHTMETA, config::Modifier::Super), 306 | (Key::KEY_LEFTALT, config::Modifier::Alt), 307 | (Key::KEY_RIGHTALT, config::Modifier::Altgr), 308 | (Key::KEY_LEFTCTRL, config::Modifier::Control), 309 | (Key::KEY_RIGHTCTRL, config::Modifier::Control), 310 | (Key::KEY_LEFTSHIFT, config::Modifier::Shift), 311 | (Key::KEY_RIGHTSHIFT, config::Modifier::Shift), 312 | ]); 313 | 314 | let repeat_cooldown_duration: u64 = args.cooldown; 315 | 316 | let mut signals = Signals::new([ 317 | SIGUSR1, SIGUSR2, SIGHUP, SIGABRT, SIGBUS, SIGCONT, SIGINT, SIGPIPE, SIGQUIT, SIGSYS, 318 | SIGTERM, SIGTRAP, SIGTSTP, SIGVTALRM, SIGXCPU, SIGXFSZ, 319 | ])?; 320 | 321 | let mut execution_is_paused = false; 322 | let mut last_hotkey: Option = None; 323 | let mut pending_release: bool = false; 324 | let mut keyboard_states = HashMap::new(); 325 | let mut keyboard_stream_map = StreamMap::new(); 326 | 327 | for (path, mut device) in keyboard_devices.into_iter() { 328 | let _ = device.grab(); 329 | let path = match path.to_str() { 330 | Some(p) => p, 331 | None => { 332 | continue; 333 | } 334 | }; 335 | keyboard_states.insert(path.to_string(), KeyboardState::new()); 336 | keyboard_stream_map.insert(path.to_string(), device.into_event_stream()?); 337 | } 338 | 339 | // The initial sleep duration is never read because last_hotkey is initialized to None 340 | let hotkey_repeat_timer = sleep(Duration::from_millis(0)); 341 | tokio::pin!(hotkey_repeat_timer); 342 | 343 | loop { 344 | select! { 345 | _ = &mut hotkey_repeat_timer, if &last_hotkey.is_some() => { 346 | let hotkey = last_hotkey.clone().unwrap(); 347 | if hotkey.keybinding.on_release { 348 | continue; 349 | } 350 | send_command(hotkey.clone(), &modes, &mut mode_stack, tx.clone()).await; 351 | hotkey_repeat_timer.as_mut().reset(Instant::now() + Duration::from_millis(repeat_cooldown_duration)); 352 | } 353 | 354 | 355 | 356 | Some(signal) = signals.next() => { 357 | match signal { 358 | SIGUSR1 => { 359 | execution_is_paused = true; 360 | for mut device in evdev::enumerate().map(|(_, device)| device).filter(check_device_is_keyboard) { 361 | let _ = device.ungrab(); 362 | } 363 | } 364 | 365 | SIGUSR2 => { 366 | execution_is_paused = false; 367 | for mut device in evdev::enumerate().map(|(_, device)| device).filter(check_device_is_keyboard) { 368 | let _ = device.grab(); 369 | } 370 | } 371 | 372 | SIGHUP => { 373 | modes = load_config(); 374 | mode_stack = vec![0]; 375 | } 376 | 377 | SIGINT => { 378 | for mut device in evdev::enumerate().map(|(_, device)| device).filter(check_device_is_keyboard) { 379 | let _ = device.ungrab(); 380 | } 381 | log::warn!("Received SIGINT signal, exiting..."); 382 | exit(1); 383 | } 384 | 385 | _ => { 386 | for mut device in evdev::enumerate().map(|(_, device)| device).filter(check_device_is_keyboard) { 387 | let _ = device.ungrab(); 388 | } 389 | 390 | log::warn!("Received signal: {:#?}", signal); 391 | log::warn!("Exiting..."); 392 | exit(1); 393 | } 394 | } 395 | } 396 | 397 | Some(Ok(event)) = udev.next() => { 398 | if !event.is_initialized() { 399 | log::warn!("Received udev event with uninitialized device."); 400 | } 401 | 402 | let node = match event.devnode() { 403 | None => { continue; }, 404 | Some(node) => { 405 | match node.to_str() { 406 | None => { continue; }, 407 | Some(node) => node, 408 | } 409 | }, 410 | }; 411 | 412 | match event.event_type() { 413 | EventType::Add => { 414 | let mut device = match Device::open(node) { 415 | Err(e) => { 416 | log::error!("Could not open evdev device at {}: {}", node, e); 417 | continue; 418 | }, 419 | Ok(device) => device 420 | }; 421 | let name = device.name().unwrap_or("[unknown]").to_string(); 422 | if arg_devices.contains(&name) || check_device_is_keyboard(&device) { 423 | log::info!("Device '{}' at '{}' added.", name, node); 424 | let _ = device.grab(); 425 | keyboard_states.insert(node.to_string(), KeyboardState::new()); 426 | keyboard_stream_map.insert(node.to_string(), device.into_event_stream()?); 427 | } 428 | } 429 | EventType::Remove => { 430 | if keyboard_stream_map.contains_key(node) { 431 | keyboard_states.remove(node); 432 | let stream = keyboard_stream_map.remove(node).expect("device not in stream_map"); 433 | let name = stream.device().name().unwrap_or("[unknown]"); 434 | log::info!("Device '{}' at '{}' removed", name, node); 435 | } 436 | } 437 | _ => { 438 | log::trace!("Ignored udev event of type: {:?}", event.event_type()); 439 | } 440 | } 441 | } 442 | 443 | Some((node, Ok(event))) = keyboard_stream_map.next() => { 444 | let keyboard_state = &mut keyboard_states.get_mut(&node).expect("device not in states map"); 445 | 446 | let key = match event.kind() { 447 | InputEventKind::Key(keycode) => keycode, 448 | InputEventKind::Switch(_) => { 449 | uinput_switches_device.emit(&[event]).unwrap(); 450 | continue 451 | } 452 | _ => { 453 | uinput_device.emit(&[event]).unwrap(); 454 | continue 455 | } 456 | }; 457 | 458 | match event.value() { 459 | // Key press 460 | 1 => { 461 | if let Some(modifier) = modifiers_map.get(&key) { 462 | keyboard_state.state_modifiers.insert(*modifier); 463 | } else { 464 | keyboard_state.state_keysyms.insert(key); 465 | } 466 | } 467 | 468 | // Key release 469 | 0 => { 470 | if last_hotkey.is_some() && pending_release { 471 | pending_release = false; 472 | send_command(last_hotkey.clone().unwrap(), &modes, &mut mode_stack, tx.clone()).await; 473 | last_hotkey = None; 474 | } 475 | if let Some(modifier) = modifiers_map.get(&key) { 476 | if let Some(hotkey) = &last_hotkey { 477 | if hotkey.modifiers().contains(modifier) { 478 | last_hotkey = None; 479 | } 480 | } 481 | keyboard_state.state_modifiers.remove(modifier); 482 | } else if keyboard_state.state_keysyms.contains(key) { 483 | if let Some(hotkey) = &last_hotkey { 484 | if key == hotkey.keysym() { 485 | last_hotkey = None; 486 | } 487 | } 488 | keyboard_state.state_keysyms.remove(key); 489 | } 490 | } 491 | 492 | _ => {} 493 | } 494 | 495 | let possible_hotkeys: Vec<&config::Hotkey> = modes[mode_stack[mode_stack.len() - 1]].hotkeys.iter() 496 | .filter(|hotkey| hotkey.modifiers().len() == keyboard_state.state_modifiers.len()) 497 | .collect(); 498 | 499 | let event_in_hotkeys = modes[mode_stack[mode_stack.len() - 1]].hotkeys.iter().any(|hotkey| { 500 | hotkey.keysym().code() == event.code() && 501 | (!keyboard_state.state_modifiers.is_empty() && hotkey.modifiers().contains(&config::Modifier::Any) || keyboard_state.state_modifiers 502 | .iter() 503 | .all(|x| hotkey.modifiers().contains(x)) && 504 | keyboard_state.state_modifiers.len() == hotkey.modifiers().len()) 505 | && !hotkey.is_send() 506 | }); 507 | 508 | // Only emit event to virtual device when swallow option is off 509 | if !modes[mode_stack[mode_stack.len()-1]].options.swallow 510 | // Don't emit event to virtual device if it's from a valid hotkey 511 | && !event_in_hotkeys { 512 | uinput_device.emit(&[event]).unwrap(); 513 | } 514 | 515 | if execution_is_paused || possible_hotkeys.is_empty() || last_hotkey.is_some() { 516 | continue; 517 | } 518 | 519 | log::debug!("state_modifiers: {:#?}", keyboard_state.state_modifiers); 520 | log::debug!("state_keysyms: {:#?}", keyboard_state.state_keysyms); 521 | log::debug!("hotkey: {:#?}", possible_hotkeys); 522 | 523 | for hotkey in possible_hotkeys { 524 | // this should check if state_modifiers and hotkey.modifiers have the same elements 525 | if (!keyboard_state.state_modifiers.is_empty() && hotkey.modifiers().contains(&config::Modifier::Any) || keyboard_state.state_modifiers.iter().all(|x| hotkey.modifiers().contains(x)) 526 | && keyboard_state.state_modifiers.len() == hotkey.modifiers().len()) 527 | && keyboard_state.state_keysyms.contains(hotkey.keysym()) 528 | { 529 | last_hotkey = Some(hotkey.clone()); 530 | if pending_release { break; } 531 | if hotkey.is_on_release() { 532 | pending_release = true; 533 | break; 534 | } 535 | send_command(hotkey.clone(), &modes, &mut mode_stack, tx.clone()).await; 536 | hotkey_repeat_timer.as_mut().reset(Instant::now() + Duration::from_millis(repeat_cooldown_duration)); 537 | continue; 538 | } 539 | } 540 | } 541 | } 542 | } 543 | } 544 | 545 | pub fn check_device_is_keyboard(device: &Device) -> bool { 546 | if device.supported_keys().is_some_and(|keys| keys.contains(Key::KEY_ENTER)) { 547 | if device.name() == Some("swhkd virtual output") { 548 | return false; 549 | } 550 | log::debug!("Keyboard: {}", device.name().unwrap(),); 551 | true 552 | } else { 553 | log::trace!("Other: {}", device.name().unwrap(),); 554 | false 555 | } 556 | } 557 | 558 | pub fn setup_swhkd(invoking_uid: u32, runtime_path: PathBuf) { 559 | // Set a sane process umask. 560 | log::trace!("Setting process umask."); 561 | umask(Mode::S_IWGRP | Mode::S_IWOTH); 562 | 563 | // Get the runtime path and create it if needed. 564 | if !Path::new(&runtime_path).exists() { 565 | match fs::create_dir_all(Path::new(&runtime_path)) { 566 | Ok(_) => { 567 | log::debug!("Created runtime directory."); 568 | match fs::set_permissions(Path::new(&runtime_path), Permissions::from_mode(0o600)) { 569 | Ok(_) => log::debug!("Set runtime directory to readonly."), 570 | Err(e) => log::error!("Failed to set runtime directory to readonly: {}", e), 571 | } 572 | } 573 | Err(e) => log::error!("Failed to create runtime directory: {}", e), 574 | } 575 | } 576 | 577 | // Get the PID file path for instance tracking. 578 | let pidfile: String = format!("{}/swhkd_{}.pid", runtime_path.to_string_lossy(), invoking_uid); 579 | if Path::new(&pidfile).exists() { 580 | log::trace!("Reading {} file and checking for running instances.", pidfile); 581 | let swhkd_pid = match fs::read_to_string(&pidfile) { 582 | Ok(swhkd_pid) => swhkd_pid, 583 | Err(e) => { 584 | log::error!("Unable to read {} to check all running instances", e); 585 | exit(1); 586 | } 587 | }; 588 | log::debug!("Previous PID: {}", swhkd_pid); 589 | 590 | // Check if swhkd is already running! 591 | let mut sys = System::new_all(); 592 | sys.refresh_all(); 593 | for (pid, process) in sys.processes() { 594 | if pid.to_string() == swhkd_pid && process.exe() == env::current_exe().unwrap() { 595 | log::error!("Swhkd is already running!"); 596 | log::error!("pid of existing swhkd process: {}", pid.to_string()); 597 | log::error!("To close the existing swhkd process, run `sudo killall swhkd`"); 598 | exit(1); 599 | } 600 | } 601 | } 602 | 603 | // Write to the pid file. 604 | match fs::write(&pidfile, id().to_string()) { 605 | Ok(_) => {} 606 | Err(e) => { 607 | log::error!("Unable to write to {}: {}", pidfile, e); 608 | exit(1); 609 | } 610 | } 611 | } 612 | 613 | pub async fn send_command( 614 | hotkey: Hotkey, 615 | modes: &[config::Mode], 616 | mode_stack: &mut Vec, 617 | tx: mpsc::Sender, 618 | ) { 619 | log::info!("Hotkey pressed: {:#?}", hotkey); 620 | let mut command = hotkey.command; 621 | if modes[*mode_stack.last().unwrap()].options.oneoff { 622 | mode_stack.pop(); 623 | } 624 | for mode in hotkey.mode_instructions.iter() { 625 | match mode { 626 | sweet::ModeInstruction::Enter(name) => { 627 | if let Some(mode_index) = modes.iter().position(|modename| modename.name.eq(name)) { 628 | mode_stack.push(mode_index); 629 | log::info!("Entering mode: {}", name); 630 | } 631 | } 632 | sweet::ModeInstruction::Escape => { 633 | mode_stack.pop(); 634 | } 635 | } 636 | } 637 | if command.ends_with(" &&") { 638 | command = command.strip_suffix(" &&").unwrap().to_string(); 639 | } 640 | 641 | match tx.send(command).await { 642 | Ok(_) => {} 643 | Err(e) => { 644 | log::error!("Failed to send command: {}", e); 645 | } 646 | } 647 | } 648 | 649 | /// Get the UID of the user that is not a system user 650 | fn get_uid() -> Result> { 651 | let status_content = fs::read_to_string(format!("/proc/{}/loginuid", std::process::id()))?; 652 | let uid = status_content.trim().parse::()?; 653 | Ok(uid) 654 | } 655 | 656 | fn get_file_paths(runtime_dir: &str) -> (String, String) { 657 | let pid_file_path = format!("{}/swhks.pid", runtime_dir); 658 | let sock_file_path = format!("{}/swhkd.sock", runtime_dir); 659 | 660 | (pid_file_path, sock_file_path) 661 | } 662 | 663 | /// Refreshes the environment variables from the server 664 | pub fn refresh_env( 665 | invoking_uid: u32, 666 | prev_hash: u64, 667 | ) -> Result<(Option, u64), Box> { 668 | // A simple placeholder for the env that is to be refreshed 669 | let env = environ::Env::construct(None); 670 | 671 | let (_pid_path, sock_path) = 672 | get_file_paths(env.xdg_runtime_dir(invoking_uid).to_str().unwrap()); 673 | 674 | let mut buff: String = String::new(); 675 | 676 | // Follows a two part process to recieve the env hash and the env itself 677 | // First part: Send a "1" as a byte to the socket to request the hash 678 | if let Ok(mut stream) = UnixStream::connect(&sock_path) { 679 | let n = stream.write(&[1])?; 680 | if n != 1 { 681 | log::error!("Failed to write to socket."); 682 | return Ok((None, prev_hash)); 683 | } 684 | stream.read_to_string(&mut buff)?; 685 | } 686 | 687 | let env_hash = buff.parse().unwrap_or_default(); 688 | 689 | // If the hash is the same as the previous hash, return early 690 | // no need to refresh the env 691 | if env_hash == prev_hash { 692 | return Ok((None, prev_hash)); 693 | } 694 | 695 | // Now that we know the env hash is different, we can request the env 696 | // Second part: Send a "2" as a byte to the socket to request the env 697 | if let Ok(mut stream) = UnixStream::connect(&sock_path) { 698 | let n = stream.write(&[2])?; 699 | if n != 1 { 700 | log::error!("Failed to write to socket."); 701 | return Ok((None, prev_hash)); 702 | } 703 | 704 | // Clear the buffer before reading 705 | buff.clear(); 706 | stream.read_to_string(&mut buff)?; 707 | } 708 | 709 | log::info!("Env refreshed"); 710 | 711 | // Construct the env from the recieved env and return it 712 | Ok((Some(environ::Env::construct(Some(&buff))), env_hash)) 713 | } 714 | -------------------------------------------------------------------------------- /swhkd/src/environ.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap, env, path::PathBuf}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Env { 5 | pub pairs: HashMap, 6 | } 7 | 8 | impl Env { 9 | /// Parses an environment string into key-value pairs. 10 | fn parse_env(env: &str) -> HashMap { 11 | env.lines() 12 | .filter_map(|line| { 13 | let mut parts = line.splitn(2, '='); 14 | Some((parts.next()?.to_string(), parts.next()?.to_string())) 15 | }) 16 | .collect() 17 | } 18 | 19 | /// Constructs an environment structure from the given string or system environment variables. 20 | pub fn construct(env: Option<&str>) -> Self { 21 | let pairs = env.map(Self::parse_env).unwrap_or_else(|| env::vars().collect()); 22 | Self { pairs } 23 | } 24 | 25 | /// Fetches the HOME directory path. 26 | pub fn fetch_home(&self) -> Option { 27 | self.pairs.get("HOME").map(PathBuf::from) 28 | } 29 | 30 | /// Fetches the XDG config path. 31 | pub fn fetch_xdg_config_path(&self) -> PathBuf { 32 | let default = self 33 | .fetch_home() 34 | .map(|home| home.join(".config")) 35 | .unwrap_or_else(|| PathBuf::from("/etc")) 36 | .to_string_lossy() // Convert PathBuf -> Cow<'_, str> 37 | .into_owned(); 38 | 39 | let xdg_config_home = 40 | self.pairs.get("XDG_CONFIG_HOME").map(String::as_str).unwrap_or(&default); 41 | 42 | PathBuf::from(xdg_config_home).join("swhkd").join("swhkdrc") 43 | } 44 | 45 | /// Fetches the XDG data path. 46 | pub fn fetch_xdg_data_path(&self) -> PathBuf { 47 | let default = self 48 | .fetch_home() 49 | .map(|home| home.join(".local/share")) 50 | .unwrap_or_else(|| PathBuf::from("/etc")) 51 | .to_string_lossy() 52 | .into_owned(); 53 | 54 | let xdg_data_home = self.pairs.get("XDG_DATA_HOME").map(String::as_str).unwrap_or(&default); 55 | 56 | PathBuf::from(xdg_data_home) 57 | } 58 | 59 | /// Fetches the XDG runtime directory path for the given user ID. 60 | pub fn xdg_runtime_dir(&self, uid: u32) -> PathBuf { 61 | let default = format!("/run/user/{}", uid); 62 | 63 | let xdg_runtime_dir = 64 | self.pairs.get("XDG_RUNTIME_DIR").map(String::as_str).unwrap_or(&default); 65 | 66 | PathBuf::from(xdg_runtime_dir) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /swhkd/src/perms.rs: -------------------------------------------------------------------------------- 1 | use nix::unistd::{Gid, Uid, User}; 2 | use std::process::exit; 3 | 4 | pub fn _drop_privileges(user_uid: u32) { 5 | let user_uid = Uid::from_raw(user_uid); 6 | let user = User::from_uid(user_uid).unwrap().unwrap(); 7 | 8 | set_initgroups(&user, user_uid.as_raw()); 9 | set_egid(user_uid.as_raw()); 10 | set_euid(user_uid.as_raw()); 11 | } 12 | 13 | pub fn raise_privileges() { 14 | let root_user = User::from_uid(Uid::from_raw(0)).unwrap().unwrap(); 15 | 16 | set_egid(0); 17 | set_euid(0); 18 | set_initgroups(&root_user, 0); 19 | } 20 | 21 | fn set_initgroups(user: &nix::unistd::User, gid: u32) { 22 | let gid = Gid::from_raw(gid); 23 | match nix::unistd::initgroups(&user.gecos, gid) { 24 | Ok(_) => log::debug!("Setting initgroups..."), 25 | Err(e) => { 26 | log::error!("Failed to set init groups: {:#?}", e); 27 | exit(1); 28 | } 29 | } 30 | } 31 | 32 | fn set_egid(gid: u32) { 33 | let gid = Gid::from_raw(gid); 34 | match nix::unistd::setegid(gid) { 35 | Ok(_) => log::debug!("Setting EGID..."), 36 | Err(e) => { 37 | log::error!("Failed to set EGID: {:#?}", e); 38 | exit(1); 39 | } 40 | } 41 | } 42 | 43 | fn set_euid(uid: u32) { 44 | let uid = Uid::from_raw(uid); 45 | match nix::unistd::seteuid(uid) { 46 | Ok(_) => log::debug!("Setting EUID..."), 47 | Err(e) => { 48 | log::error!("Failed to set EUID: {:#?}", e); 49 | exit(1); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /swhkd/src/uinput.rs: -------------------------------------------------------------------------------- 1 | use evdev::{ 2 | uinput::{VirtualDevice, VirtualDeviceBuilder}, 3 | AttributeSet, Key, RelativeAxisType, SwitchType, 4 | }; 5 | 6 | #[cfg(not(feature = "no_rfkill"))] 7 | use nix::ioctl_none; 8 | #[cfg(not(feature = "no_rfkill"))] 9 | use std::fs::File; 10 | #[cfg(not(feature = "no_rfkill"))] 11 | use std::os::unix::io::AsRawFd; 12 | 13 | #[cfg(not(feature = "no_rfkill"))] 14 | ioctl_none!(rfkill_noinput, b'R', 1); 15 | 16 | pub fn create_uinput_device() -> Result> { 17 | let keys: AttributeSet = get_all_keys().iter().copied().collect(); 18 | 19 | let relative_axes: AttributeSet = 20 | get_all_relative_axes().iter().copied().collect(); 21 | 22 | let device = VirtualDeviceBuilder::new()? 23 | .name("swhkd virtual output") 24 | .with_keys(&keys)? 25 | .with_relative_axes(&relative_axes)? 26 | .build()?; 27 | Ok(device) 28 | } 29 | 30 | pub fn create_uinput_switches_device() -> Result> { 31 | let switches: AttributeSet = get_all_switches().iter().copied().collect(); 32 | 33 | // We have to disable rfkill-input to avoid blocking all radio devices. When 34 | // a new device (virtual or physical) with the SW_RFKILL_ALL capability bit 35 | // set appears, rfkill reacts immediately depending on the value bit. This 36 | // value bit defaults to unset, which causes rfkill to use its default mode 37 | // (which is eop - emergency power off). The uinput API does not give any 38 | // way to set the corresponding value bit before creating the device, and we 39 | // have no way to avoid rfkill acting upon the device creation or to change 40 | // its default mode. Thus, we disable rfkill-input temporarily, hopefully 41 | // fast enough that it won't impact anyone. rfkill-input will be enabled 42 | // again when the file gets closed. 43 | // Implemented as feature for it causes issues with radio on some platforms. 44 | #[cfg(not(feature = "no_rfkill"))] 45 | { 46 | let rfkill_file = File::open("/dev/rfkill")?; 47 | unsafe { 48 | rfkill_noinput(rfkill_file.as_raw_fd())?; 49 | } 50 | } 51 | 52 | let device = VirtualDeviceBuilder::new()? 53 | .name("swhkd switches virtual output") 54 | .with_switches(&switches)? 55 | .build()?; 56 | Ok(device) 57 | } 58 | pub fn get_all_keys() -> &'static [Key] { 59 | &[ 60 | evdev::Key::KEY_RESERVED, 61 | evdev::Key::KEY_ESC, 62 | evdev::Key::KEY_1, 63 | evdev::Key::KEY_2, 64 | evdev::Key::KEY_3, 65 | evdev::Key::KEY_4, 66 | evdev::Key::KEY_5, 67 | evdev::Key::KEY_6, 68 | evdev::Key::KEY_7, 69 | evdev::Key::KEY_8, 70 | evdev::Key::KEY_9, 71 | evdev::Key::KEY_0, 72 | evdev::Key::KEY_MINUS, 73 | evdev::Key::KEY_EQUAL, 74 | evdev::Key::KEY_BACKSPACE, 75 | evdev::Key::KEY_TAB, 76 | evdev::Key::KEY_Q, 77 | evdev::Key::KEY_W, 78 | evdev::Key::KEY_E, 79 | evdev::Key::KEY_R, 80 | evdev::Key::KEY_T, 81 | evdev::Key::KEY_Y, 82 | evdev::Key::KEY_U, 83 | evdev::Key::KEY_I, 84 | evdev::Key::KEY_O, 85 | evdev::Key::KEY_P, 86 | evdev::Key::KEY_LEFTBRACE, 87 | evdev::Key::KEY_RIGHTBRACE, 88 | evdev::Key::KEY_ENTER, 89 | evdev::Key::KEY_LEFTCTRL, 90 | evdev::Key::KEY_A, 91 | evdev::Key::KEY_S, 92 | evdev::Key::KEY_D, 93 | evdev::Key::KEY_F, 94 | evdev::Key::KEY_G, 95 | evdev::Key::KEY_H, 96 | evdev::Key::KEY_J, 97 | evdev::Key::KEY_K, 98 | evdev::Key::KEY_L, 99 | evdev::Key::KEY_SEMICOLON, 100 | evdev::Key::KEY_APOSTROPHE, 101 | evdev::Key::KEY_GRAVE, 102 | evdev::Key::KEY_LEFTSHIFT, 103 | evdev::Key::KEY_BACKSLASH, 104 | evdev::Key::KEY_Z, 105 | evdev::Key::KEY_X, 106 | evdev::Key::KEY_C, 107 | evdev::Key::KEY_V, 108 | evdev::Key::KEY_B, 109 | evdev::Key::KEY_N, 110 | evdev::Key::KEY_M, 111 | evdev::Key::KEY_COMMA, 112 | evdev::Key::KEY_DOT, 113 | evdev::Key::KEY_SLASH, 114 | evdev::Key::KEY_RIGHTSHIFT, 115 | evdev::Key::KEY_KPASTERISK, 116 | evdev::Key::KEY_LEFTALT, 117 | evdev::Key::KEY_SPACE, 118 | evdev::Key::KEY_CAPSLOCK, 119 | evdev::Key::KEY_F1, 120 | evdev::Key::KEY_F2, 121 | evdev::Key::KEY_F3, 122 | evdev::Key::KEY_F4, 123 | evdev::Key::KEY_F5, 124 | evdev::Key::KEY_F6, 125 | evdev::Key::KEY_F7, 126 | evdev::Key::KEY_F8, 127 | evdev::Key::KEY_F9, 128 | evdev::Key::KEY_F10, 129 | evdev::Key::KEY_NUMLOCK, 130 | evdev::Key::KEY_SCROLLLOCK, 131 | evdev::Key::KEY_KP7, 132 | evdev::Key::KEY_KP8, 133 | evdev::Key::KEY_KP9, 134 | evdev::Key::KEY_KPMINUS, 135 | evdev::Key::KEY_KP4, 136 | evdev::Key::KEY_KP5, 137 | evdev::Key::KEY_KP6, 138 | evdev::Key::KEY_KPPLUS, 139 | evdev::Key::KEY_KP1, 140 | evdev::Key::KEY_KP2, 141 | evdev::Key::KEY_KP3, 142 | evdev::Key::KEY_KP0, 143 | evdev::Key::KEY_KPDOT, 144 | evdev::Key::KEY_ZENKAKUHANKAKU, 145 | evdev::Key::KEY_102ND, 146 | evdev::Key::KEY_F11, 147 | evdev::Key::KEY_F12, 148 | evdev::Key::KEY_RO, 149 | evdev::Key::KEY_KATAKANA, 150 | evdev::Key::KEY_HIRAGANA, 151 | evdev::Key::KEY_HENKAN, 152 | evdev::Key::KEY_KATAKANAHIRAGANA, 153 | evdev::Key::KEY_MUHENKAN, 154 | evdev::Key::KEY_KPJPCOMMA, 155 | evdev::Key::KEY_KPENTER, 156 | evdev::Key::KEY_RIGHTCTRL, 157 | evdev::Key::KEY_KPSLASH, 158 | evdev::Key::KEY_SYSRQ, 159 | evdev::Key::KEY_RIGHTALT, 160 | evdev::Key::KEY_LINEFEED, 161 | evdev::Key::KEY_HOME, 162 | evdev::Key::KEY_UP, 163 | evdev::Key::KEY_PAGEUP, 164 | evdev::Key::KEY_LEFT, 165 | evdev::Key::KEY_RIGHT, 166 | evdev::Key::KEY_END, 167 | evdev::Key::KEY_DOWN, 168 | evdev::Key::KEY_PAGEDOWN, 169 | evdev::Key::KEY_INSERT, 170 | evdev::Key::KEY_DELETE, 171 | evdev::Key::KEY_MACRO, 172 | evdev::Key::KEY_MUTE, 173 | evdev::Key::KEY_VOLUMEDOWN, 174 | evdev::Key::KEY_VOLUMEUP, 175 | evdev::Key::KEY_POWER, 176 | evdev::Key::KEY_KPEQUAL, 177 | evdev::Key::KEY_KPPLUSMINUS, 178 | evdev::Key::KEY_PAUSE, 179 | evdev::Key::KEY_SCALE, 180 | evdev::Key::KEY_KPCOMMA, 181 | evdev::Key::KEY_HANGEUL, 182 | evdev::Key::KEY_HANJA, 183 | evdev::Key::KEY_YEN, 184 | evdev::Key::KEY_LEFTMETA, 185 | evdev::Key::KEY_RIGHTMETA, 186 | evdev::Key::KEY_COMPOSE, 187 | evdev::Key::KEY_STOP, 188 | evdev::Key::KEY_AGAIN, 189 | evdev::Key::KEY_PROPS, 190 | evdev::Key::KEY_UNDO, 191 | evdev::Key::KEY_FRONT, 192 | evdev::Key::KEY_COPY, 193 | evdev::Key::KEY_OPEN, 194 | evdev::Key::KEY_PASTE, 195 | evdev::Key::KEY_FIND, 196 | evdev::Key::KEY_CUT, 197 | evdev::Key::KEY_HELP, 198 | evdev::Key::KEY_MENU, 199 | evdev::Key::KEY_CALC, 200 | evdev::Key::KEY_SETUP, 201 | evdev::Key::KEY_SLEEP, 202 | evdev::Key::KEY_WAKEUP, 203 | evdev::Key::KEY_FILE, 204 | evdev::Key::KEY_SENDFILE, 205 | evdev::Key::KEY_DELETEFILE, 206 | evdev::Key::KEY_XFER, 207 | evdev::Key::KEY_PROG1, 208 | evdev::Key::KEY_PROG2, 209 | evdev::Key::KEY_WWW, 210 | evdev::Key::KEY_MSDOS, 211 | evdev::Key::KEY_COFFEE, 212 | evdev::Key::KEY_DIRECTION, 213 | evdev::Key::KEY_ROTATE_DISPLAY, 214 | evdev::Key::KEY_CYCLEWINDOWS, 215 | evdev::Key::KEY_MAIL, 216 | evdev::Key::KEY_BOOKMARKS, 217 | evdev::Key::KEY_COMPUTER, 218 | evdev::Key::KEY_BACK, 219 | evdev::Key::KEY_FORWARD, 220 | evdev::Key::KEY_CLOSECD, 221 | evdev::Key::KEY_EJECTCD, 222 | evdev::Key::KEY_EJECTCLOSECD, 223 | evdev::Key::KEY_NEXTSONG, 224 | evdev::Key::KEY_PLAYPAUSE, 225 | evdev::Key::KEY_PREVIOUSSONG, 226 | evdev::Key::KEY_STOPCD, 227 | evdev::Key::KEY_RECORD, 228 | evdev::Key::KEY_REWIND, 229 | evdev::Key::KEY_PHONE, 230 | evdev::Key::KEY_ISO, 231 | evdev::Key::KEY_CONFIG, 232 | evdev::Key::KEY_HOMEPAGE, 233 | evdev::Key::KEY_REFRESH, 234 | evdev::Key::KEY_EXIT, 235 | evdev::Key::KEY_MOVE, 236 | evdev::Key::KEY_EDIT, 237 | evdev::Key::KEY_SCROLLUP, 238 | evdev::Key::KEY_SCROLLDOWN, 239 | evdev::Key::KEY_KPLEFTPAREN, 240 | evdev::Key::KEY_KPRIGHTPAREN, 241 | evdev::Key::KEY_NEW, 242 | evdev::Key::KEY_REDO, 243 | evdev::Key::KEY_F13, 244 | evdev::Key::KEY_F14, 245 | evdev::Key::KEY_F15, 246 | evdev::Key::KEY_F16, 247 | evdev::Key::KEY_F17, 248 | evdev::Key::KEY_F18, 249 | evdev::Key::KEY_F19, 250 | evdev::Key::KEY_F20, 251 | evdev::Key::KEY_F21, 252 | evdev::Key::KEY_F22, 253 | evdev::Key::KEY_F23, 254 | evdev::Key::KEY_F24, 255 | evdev::Key::KEY_PLAYCD, 256 | evdev::Key::KEY_PAUSECD, 257 | evdev::Key::KEY_PROG3, 258 | evdev::Key::KEY_PROG4, 259 | evdev::Key::KEY_DASHBOARD, 260 | evdev::Key::KEY_SUSPEND, 261 | evdev::Key::KEY_CLOSE, 262 | evdev::Key::KEY_PLAY, 263 | evdev::Key::KEY_FASTFORWARD, 264 | evdev::Key::KEY_BASSBOOST, 265 | evdev::Key::KEY_PRINT, 266 | evdev::Key::KEY_HP, 267 | evdev::Key::KEY_CAMERA, 268 | evdev::Key::KEY_SOUND, 269 | evdev::Key::KEY_QUESTION, 270 | evdev::Key::KEY_EMAIL, 271 | evdev::Key::KEY_CHAT, 272 | evdev::Key::KEY_SEARCH, 273 | evdev::Key::KEY_CONNECT, 274 | evdev::Key::KEY_FINANCE, 275 | evdev::Key::KEY_SPORT, 276 | evdev::Key::KEY_SHOP, 277 | evdev::Key::KEY_ALTERASE, 278 | evdev::Key::KEY_CANCEL, 279 | evdev::Key::KEY_BRIGHTNESSDOWN, 280 | evdev::Key::KEY_BRIGHTNESSUP, 281 | evdev::Key::KEY_MEDIA, 282 | evdev::Key::KEY_SWITCHVIDEOMODE, 283 | evdev::Key::KEY_KBDILLUMTOGGLE, 284 | evdev::Key::KEY_KBDILLUMDOWN, 285 | evdev::Key::KEY_KBDILLUMUP, 286 | evdev::Key::KEY_SEND, 287 | evdev::Key::KEY_REPLY, 288 | evdev::Key::KEY_FORWARDMAIL, 289 | evdev::Key::KEY_SAVE, 290 | evdev::Key::KEY_DOCUMENTS, 291 | evdev::Key::KEY_BATTERY, 292 | evdev::Key::KEY_BLUETOOTH, 293 | evdev::Key::KEY_WLAN, 294 | evdev::Key::KEY_UWB, 295 | evdev::Key::KEY_UNKNOWN, 296 | evdev::Key::KEY_VIDEO_NEXT, 297 | evdev::Key::KEY_VIDEO_PREV, 298 | evdev::Key::KEY_BRIGHTNESS_CYCLE, 299 | evdev::Key::KEY_BRIGHTNESS_AUTO, 300 | evdev::Key::KEY_DISPLAY_OFF, 301 | evdev::Key::KEY_WWAN, 302 | evdev::Key::KEY_RFKILL, 303 | evdev::Key::KEY_MICMUTE, 304 | evdev::Key::BTN_0, 305 | evdev::Key::BTN_1, 306 | evdev::Key::BTN_2, 307 | evdev::Key::BTN_3, 308 | evdev::Key::BTN_4, 309 | evdev::Key::BTN_5, 310 | evdev::Key::BTN_6, 311 | evdev::Key::BTN_7, 312 | evdev::Key::BTN_8, 313 | evdev::Key::BTN_9, 314 | evdev::Key::BTN_LEFT, 315 | evdev::Key::BTN_RIGHT, 316 | evdev::Key::BTN_MIDDLE, 317 | evdev::Key::BTN_SIDE, 318 | evdev::Key::BTN_EXTRA, 319 | evdev::Key::BTN_FORWARD, 320 | evdev::Key::BTN_BACK, 321 | evdev::Key::BTN_TASK, 322 | evdev::Key::BTN_TRIGGER, 323 | evdev::Key::BTN_THUMB, 324 | evdev::Key::BTN_THUMB2, 325 | evdev::Key::BTN_TOP, 326 | evdev::Key::BTN_TOP2, 327 | evdev::Key::BTN_PINKIE, 328 | evdev::Key::BTN_BASE, 329 | evdev::Key::BTN_BASE2, 330 | evdev::Key::BTN_BASE3, 331 | evdev::Key::BTN_BASE4, 332 | evdev::Key::BTN_BASE5, 333 | evdev::Key::BTN_BASE6, 334 | evdev::Key::BTN_DEAD, 335 | evdev::Key::BTN_SOUTH, 336 | evdev::Key::BTN_EAST, 337 | evdev::Key::BTN_C, 338 | evdev::Key::BTN_NORTH, 339 | evdev::Key::BTN_WEST, 340 | evdev::Key::BTN_Z, 341 | evdev::Key::BTN_TL, 342 | evdev::Key::BTN_TR, 343 | evdev::Key::BTN_TL2, 344 | evdev::Key::BTN_TR2, 345 | evdev::Key::BTN_SELECT, 346 | evdev::Key::BTN_START, 347 | evdev::Key::BTN_MODE, 348 | evdev::Key::BTN_THUMBL, 349 | evdev::Key::BTN_THUMBR, 350 | evdev::Key::BTN_TOOL_PEN, 351 | evdev::Key::BTN_TOOL_RUBBER, 352 | evdev::Key::BTN_TOOL_BRUSH, 353 | evdev::Key::BTN_TOOL_PENCIL, 354 | evdev::Key::BTN_TOOL_AIRBRUSH, 355 | evdev::Key::BTN_TOOL_FINGER, 356 | evdev::Key::BTN_TOOL_MOUSE, 357 | evdev::Key::BTN_TOOL_LENS, 358 | evdev::Key::BTN_TOOL_QUINTTAP, 359 | evdev::Key::BTN_TOUCH, 360 | evdev::Key::BTN_STYLUS, 361 | evdev::Key::BTN_STYLUS2, 362 | evdev::Key::BTN_TOOL_DOUBLETAP, 363 | evdev::Key::BTN_TOOL_TRIPLETAP, 364 | evdev::Key::BTN_TOOL_QUADTAP, 365 | evdev::Key::BTN_GEAR_DOWN, 366 | evdev::Key::BTN_GEAR_UP, 367 | evdev::Key::KEY_OK, 368 | evdev::Key::KEY_SELECT, 369 | evdev::Key::KEY_GOTO, 370 | evdev::Key::KEY_CLEAR, 371 | evdev::Key::KEY_POWER2, 372 | evdev::Key::KEY_OPTION, 373 | evdev::Key::KEY_INFO, 374 | evdev::Key::KEY_TIME, 375 | evdev::Key::KEY_VENDOR, 376 | evdev::Key::KEY_ARCHIVE, 377 | evdev::Key::KEY_PROGRAM, 378 | evdev::Key::KEY_CHANNEL, 379 | evdev::Key::KEY_FAVORITES, 380 | evdev::Key::KEY_EPG, 381 | evdev::Key::KEY_PVR, 382 | evdev::Key::KEY_MHP, 383 | evdev::Key::KEY_LANGUAGE, 384 | evdev::Key::KEY_TITLE, 385 | evdev::Key::KEY_SUBTITLE, 386 | evdev::Key::KEY_ANGLE, 387 | evdev::Key::KEY_ZOOM, 388 | evdev::Key::KEY_FULL_SCREEN, 389 | evdev::Key::KEY_MODE, 390 | evdev::Key::KEY_KEYBOARD, 391 | evdev::Key::KEY_SCREEN, 392 | evdev::Key::KEY_PC, 393 | evdev::Key::KEY_TV, 394 | evdev::Key::KEY_TV2, 395 | evdev::Key::KEY_VCR, 396 | evdev::Key::KEY_VCR2, 397 | evdev::Key::KEY_SAT, 398 | evdev::Key::KEY_SAT2, 399 | evdev::Key::KEY_CD, 400 | evdev::Key::KEY_TAPE, 401 | evdev::Key::KEY_RADIO, 402 | evdev::Key::KEY_TUNER, 403 | evdev::Key::KEY_PLAYER, 404 | evdev::Key::KEY_TEXT, 405 | evdev::Key::KEY_DVD, 406 | evdev::Key::KEY_AUX, 407 | evdev::Key::KEY_MP3, 408 | evdev::Key::KEY_AUDIO, 409 | evdev::Key::KEY_VIDEO, 410 | evdev::Key::KEY_DIRECTORY, 411 | evdev::Key::KEY_LIST, 412 | evdev::Key::KEY_MEMO, 413 | evdev::Key::KEY_CALENDAR, 414 | evdev::Key::KEY_RED, 415 | evdev::Key::KEY_GREEN, 416 | evdev::Key::KEY_YELLOW, 417 | evdev::Key::KEY_BLUE, 418 | evdev::Key::KEY_CHANNELUP, 419 | evdev::Key::KEY_CHANNELDOWN, 420 | evdev::Key::KEY_FIRST, 421 | evdev::Key::KEY_LAST, 422 | evdev::Key::KEY_AB, 423 | evdev::Key::KEY_NEXT, 424 | evdev::Key::KEY_RESTART, 425 | evdev::Key::KEY_SLOW, 426 | evdev::Key::KEY_SHUFFLE, 427 | evdev::Key::KEY_BREAK, 428 | evdev::Key::KEY_PREVIOUS, 429 | evdev::Key::KEY_DIGITS, 430 | evdev::Key::KEY_TEEN, 431 | evdev::Key::KEY_TWEN, 432 | evdev::Key::KEY_VIDEOPHONE, 433 | evdev::Key::KEY_GAMES, 434 | evdev::Key::KEY_ZOOMIN, 435 | evdev::Key::KEY_ZOOMOUT, 436 | evdev::Key::KEY_ZOOMRESET, 437 | evdev::Key::KEY_WORDPROCESSOR, 438 | evdev::Key::KEY_EDITOR, 439 | evdev::Key::KEY_SPREADSHEET, 440 | evdev::Key::KEY_GRAPHICSEDITOR, 441 | evdev::Key::KEY_PRESENTATION, 442 | evdev::Key::KEY_DATABASE, 443 | evdev::Key::KEY_NEWS, 444 | evdev::Key::KEY_VOICEMAIL, 445 | evdev::Key::KEY_ADDRESSBOOK, 446 | evdev::Key::KEY_MESSENGER, 447 | evdev::Key::KEY_DISPLAYTOGGLE, 448 | evdev::Key::KEY_SPELLCHECK, 449 | evdev::Key::KEY_LOGOFF, 450 | evdev::Key::KEY_DOLLAR, 451 | evdev::Key::KEY_EURO, 452 | evdev::Key::KEY_FRAMEBACK, 453 | evdev::Key::KEY_FRAMEFORWARD, 454 | evdev::Key::KEY_CONTEXT_MENU, 455 | evdev::Key::KEY_MEDIA_REPEAT, 456 | evdev::Key::KEY_10CHANNELSUP, 457 | evdev::Key::KEY_10CHANNELSDOWN, 458 | evdev::Key::KEY_IMAGES, 459 | evdev::Key::KEY_DEL_EOL, 460 | evdev::Key::KEY_DEL_EOS, 461 | evdev::Key::KEY_INS_LINE, 462 | evdev::Key::KEY_DEL_LINE, 463 | evdev::Key::KEY_FN, 464 | evdev::Key::KEY_FN_ESC, 465 | evdev::Key::KEY_FN_F1, 466 | evdev::Key::KEY_FN_F2, 467 | evdev::Key::KEY_FN_F3, 468 | evdev::Key::KEY_FN_F4, 469 | evdev::Key::KEY_FN_F5, 470 | evdev::Key::KEY_FN_F6, 471 | evdev::Key::KEY_FN_F7, 472 | evdev::Key::KEY_FN_F8, 473 | evdev::Key::KEY_FN_F9, 474 | evdev::Key::KEY_FN_F10, 475 | evdev::Key::KEY_FN_F11, 476 | evdev::Key::KEY_FN_F12, 477 | evdev::Key::KEY_FN_1, 478 | evdev::Key::KEY_FN_2, 479 | evdev::Key::KEY_FN_D, 480 | evdev::Key::KEY_FN_E, 481 | evdev::Key::KEY_FN_F, 482 | evdev::Key::KEY_FN_S, 483 | evdev::Key::KEY_FN_B, 484 | evdev::Key::KEY_BRL_DOT1, 485 | evdev::Key::KEY_BRL_DOT2, 486 | evdev::Key::KEY_BRL_DOT3, 487 | evdev::Key::KEY_BRL_DOT4, 488 | evdev::Key::KEY_BRL_DOT5, 489 | evdev::Key::KEY_BRL_DOT6, 490 | evdev::Key::KEY_BRL_DOT7, 491 | evdev::Key::KEY_BRL_DOT8, 492 | evdev::Key::KEY_BRL_DOT9, 493 | evdev::Key::KEY_BRL_DOT10, 494 | evdev::Key::KEY_NUMERIC_0, 495 | evdev::Key::KEY_NUMERIC_1, 496 | evdev::Key::KEY_NUMERIC_2, 497 | evdev::Key::KEY_NUMERIC_3, 498 | evdev::Key::KEY_NUMERIC_4, 499 | evdev::Key::KEY_NUMERIC_5, 500 | evdev::Key::KEY_NUMERIC_6, 501 | evdev::Key::KEY_NUMERIC_7, 502 | evdev::Key::KEY_NUMERIC_8, 503 | evdev::Key::KEY_NUMERIC_9, 504 | evdev::Key::KEY_NUMERIC_STAR, 505 | evdev::Key::KEY_NUMERIC_POUND, 506 | evdev::Key::KEY_NUMERIC_A, 507 | evdev::Key::KEY_NUMERIC_B, 508 | evdev::Key::KEY_NUMERIC_C, 509 | evdev::Key::KEY_NUMERIC_D, 510 | evdev::Key::KEY_CAMERA_FOCUS, 511 | evdev::Key::KEY_WPS_BUTTON, 512 | evdev::Key::KEY_TOUCHPAD_TOGGLE, 513 | evdev::Key::KEY_TOUCHPAD_ON, 514 | evdev::Key::KEY_TOUCHPAD_OFF, 515 | evdev::Key::KEY_CAMERA_ZOOMIN, 516 | evdev::Key::KEY_CAMERA_ZOOMOUT, 517 | evdev::Key::KEY_CAMERA_UP, 518 | evdev::Key::KEY_CAMERA_DOWN, 519 | evdev::Key::KEY_CAMERA_LEFT, 520 | evdev::Key::KEY_CAMERA_RIGHT, 521 | evdev::Key::KEY_ATTENDANT_ON, 522 | evdev::Key::KEY_ATTENDANT_OFF, 523 | evdev::Key::KEY_ATTENDANT_TOGGLE, 524 | evdev::Key::KEY_LIGHTS_TOGGLE, 525 | evdev::Key::BTN_DPAD_UP, 526 | evdev::Key::BTN_DPAD_DOWN, 527 | evdev::Key::BTN_DPAD_LEFT, 528 | evdev::Key::BTN_DPAD_RIGHT, 529 | evdev::Key::KEY_ALS_TOGGLE, 530 | evdev::Key::KEY_BUTTONCONFIG, 531 | evdev::Key::KEY_TASKMANAGER, 532 | evdev::Key::KEY_JOURNAL, 533 | evdev::Key::KEY_CONTROLPANEL, 534 | evdev::Key::KEY_APPSELECT, 535 | evdev::Key::KEY_SCREENSAVER, 536 | evdev::Key::KEY_VOICECOMMAND, 537 | evdev::Key::KEY_ASSISTANT, 538 | evdev::Key::KEY_KBD_LAYOUT_NEXT, 539 | evdev::Key::KEY_BRIGHTNESS_MIN, 540 | evdev::Key::KEY_BRIGHTNESS_MAX, 541 | evdev::Key::KEY_KBDINPUTASSIST_PREV, 542 | evdev::Key::KEY_KBDINPUTASSIST_NEXT, 543 | evdev::Key::KEY_KBDINPUTASSIST_PREVGROUP, 544 | evdev::Key::KEY_KBDINPUTASSIST_NEXTGROUP, 545 | evdev::Key::KEY_KBDINPUTASSIST_ACCEPT, 546 | evdev::Key::KEY_KBDINPUTASSIST_CANCEL, 547 | evdev::Key::KEY_RIGHT_UP, 548 | evdev::Key::KEY_RIGHT_DOWN, 549 | evdev::Key::KEY_LEFT_UP, 550 | evdev::Key::KEY_LEFT_DOWN, 551 | evdev::Key::KEY_ROOT_MENU, 552 | evdev::Key::KEY_MEDIA_TOP_MENU, 553 | evdev::Key::KEY_NUMERIC_11, 554 | evdev::Key::KEY_NUMERIC_12, 555 | evdev::Key::KEY_AUDIO_DESC, 556 | evdev::Key::KEY_3D_MODE, 557 | evdev::Key::KEY_NEXT_FAVORITE, 558 | evdev::Key::KEY_STOP_RECORD, 559 | evdev::Key::KEY_PAUSE_RECORD, 560 | evdev::Key::KEY_VOD, 561 | evdev::Key::KEY_UNMUTE, 562 | evdev::Key::KEY_FASTREVERSE, 563 | evdev::Key::KEY_SLOWREVERSE, 564 | evdev::Key::KEY_DATA, 565 | evdev::Key::KEY_ONSCREEN_KEYBOARD, 566 | evdev::Key::KEY_PRIVACY_SCREEN_TOGGLE, 567 | evdev::Key::KEY_SELECTIVE_SCREENSHOT, 568 | evdev::Key::BTN_TRIGGER_HAPPY1, 569 | evdev::Key::BTN_TRIGGER_HAPPY2, 570 | evdev::Key::BTN_TRIGGER_HAPPY3, 571 | evdev::Key::BTN_TRIGGER_HAPPY4, 572 | evdev::Key::BTN_TRIGGER_HAPPY5, 573 | evdev::Key::BTN_TRIGGER_HAPPY6, 574 | evdev::Key::BTN_TRIGGER_HAPPY7, 575 | evdev::Key::BTN_TRIGGER_HAPPY8, 576 | evdev::Key::BTN_TRIGGER_HAPPY9, 577 | evdev::Key::BTN_TRIGGER_HAPPY10, 578 | evdev::Key::BTN_TRIGGER_HAPPY11, 579 | evdev::Key::BTN_TRIGGER_HAPPY12, 580 | evdev::Key::BTN_TRIGGER_HAPPY13, 581 | evdev::Key::BTN_TRIGGER_HAPPY14, 582 | evdev::Key::BTN_TRIGGER_HAPPY15, 583 | evdev::Key::BTN_TRIGGER_HAPPY16, 584 | evdev::Key::BTN_TRIGGER_HAPPY17, 585 | evdev::Key::BTN_TRIGGER_HAPPY18, 586 | evdev::Key::BTN_TRIGGER_HAPPY19, 587 | evdev::Key::BTN_TRIGGER_HAPPY20, 588 | evdev::Key::BTN_TRIGGER_HAPPY21, 589 | evdev::Key::BTN_TRIGGER_HAPPY22, 590 | evdev::Key::BTN_TRIGGER_HAPPY23, 591 | evdev::Key::BTN_TRIGGER_HAPPY24, 592 | evdev::Key::BTN_TRIGGER_HAPPY25, 593 | evdev::Key::BTN_TRIGGER_HAPPY26, 594 | evdev::Key::BTN_TRIGGER_HAPPY27, 595 | evdev::Key::BTN_TRIGGER_HAPPY28, 596 | evdev::Key::BTN_TRIGGER_HAPPY29, 597 | evdev::Key::BTN_TRIGGER_HAPPY30, 598 | evdev::Key::BTN_TRIGGER_HAPPY31, 599 | evdev::Key::BTN_TRIGGER_HAPPY32, 600 | evdev::Key::BTN_TRIGGER_HAPPY33, 601 | evdev::Key::BTN_TRIGGER_HAPPY34, 602 | evdev::Key::BTN_TRIGGER_HAPPY35, 603 | evdev::Key::BTN_TRIGGER_HAPPY36, 604 | evdev::Key::BTN_TRIGGER_HAPPY37, 605 | evdev::Key::BTN_TRIGGER_HAPPY38, 606 | evdev::Key::BTN_TRIGGER_HAPPY39, 607 | evdev::Key::BTN_TRIGGER_HAPPY40, 608 | ] 609 | } 610 | 611 | pub fn get_all_relative_axes() -> &'static [RelativeAxisType] { 612 | &[ 613 | RelativeAxisType::REL_X, 614 | RelativeAxisType::REL_Y, 615 | RelativeAxisType::REL_Z, 616 | RelativeAxisType::REL_RX, 617 | RelativeAxisType::REL_RY, 618 | RelativeAxisType::REL_RZ, 619 | RelativeAxisType::REL_HWHEEL, 620 | RelativeAxisType::REL_DIAL, 621 | RelativeAxisType::REL_WHEEL, 622 | RelativeAxisType::REL_MISC, 623 | RelativeAxisType::REL_RESERVED, 624 | RelativeAxisType::REL_WHEEL_HI_RES, 625 | RelativeAxisType::REL_HWHEEL_HI_RES, 626 | ] 627 | } 628 | 629 | pub fn get_all_switches() -> &'static [SwitchType] { 630 | &[ 631 | SwitchType::SW_LID, 632 | SwitchType::SW_TABLET_MODE, 633 | SwitchType::SW_HEADPHONE_INSERT, 634 | #[cfg(not(feature = "no_rfkill"))] 635 | SwitchType::SW_RFKILL_ALL, 636 | SwitchType::SW_MICROPHONE_INSERT, 637 | SwitchType::SW_DOCK, 638 | SwitchType::SW_LINEOUT_INSERT, 639 | SwitchType::SW_JACK_PHYSICAL_INSERT, 640 | SwitchType::SW_VIDEOOUT_INSERT, 641 | SwitchType::SW_CAMERA_LENS_COVER, 642 | SwitchType::SW_KEYPAD_SLIDE, 643 | SwitchType::SW_FRONT_PROXIMITY, 644 | SwitchType::SW_ROTATE_LOCK, 645 | SwitchType::SW_LINEIN_INSERT, 646 | SwitchType::SW_MUTE_DEVICE, 647 | SwitchType::SW_PEN_INSERTED, 648 | SwitchType::SW_MACHINE_COVER, 649 | ] 650 | } 651 | -------------------------------------------------------------------------------- /swhks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Server for swhkd to run shell commands at invoking user level." 3 | edition = "2021" 4 | license = "BSD-2-Clause" 5 | name = "swhks" 6 | version = "1.3.0-dev" 7 | authors = [ 8 | "Shinyzenith \n", 9 | "Angelo Fallaria \n", 10 | "EdenQwQ \n", 11 | ] 12 | 13 | [dependencies] 14 | env_logger = "0.9.0" 15 | log = "0.4.14" 16 | nix = "0.23.1" 17 | sysinfo = "0.23.5" 18 | clap = { version = "4.1.0", features = ["derive"] } 19 | 20 | [[bin]] 21 | name = "swhks" 22 | path = "src/main.rs" 23 | -------------------------------------------------------------------------------- /swhks/src/environ.rs: -------------------------------------------------------------------------------- 1 | //! Environ.rs 2 | //! Defines modules and structs for handling environment variables and paths. 3 | 4 | use std::{env::VarError, path::PathBuf}; 5 | 6 | use nix::unistd; 7 | 8 | // The main struct for handling environment variables. 9 | // Contains the values of the environment variables in the form of PathBuffers. 10 | pub struct Env { 11 | pub data_home: PathBuf, 12 | pub home: PathBuf, 13 | pub runtime_dir: PathBuf, 14 | } 15 | 16 | /// Error type for the Env struct. 17 | /// Contains all the possible errors that can occur when trying to get an environment variable. 18 | #[derive(Debug)] 19 | pub enum EnvError { 20 | DataHomeNotSet, 21 | HomeNotSet, 22 | RuntimeDirNotSet, 23 | PathNotFound, 24 | GenericError(String), 25 | } 26 | 27 | impl Env { 28 | /// Constructs a new Env struct. 29 | /// This function is called only once and the result is stored in a static variable. 30 | pub fn construct() -> Self { 31 | let home = match Self::get_env("HOME") { 32 | Ok(val) => val, 33 | Err(_) => { 34 | eprintln!("HOME Variable is not set/found, cannot fall back on hardcoded path for XDG_DATA_HOME."); 35 | std::process::exit(1); 36 | } 37 | }; 38 | 39 | let data_home = match Self::get_env("XDG_DATA_HOME") { 40 | Ok(val) => val, 41 | Err(e) => match e { 42 | EnvError::DataHomeNotSet | EnvError::PathNotFound => { 43 | log::warn!( 44 | "XDG_DATA_HOME Variable is not set, falling back on hardcoded path." 45 | ); 46 | home.join(".local/share") 47 | } 48 | _ => panic!("Unexpected error: {:#?}", e), 49 | }, 50 | }; 51 | 52 | let runtime_dir = match Self::get_env("XDG_RUNTIME_DIR") { 53 | Ok(val) => val, 54 | Err(e) => match e { 55 | EnvError::RuntimeDirNotSet | EnvError::PathNotFound => { 56 | log::warn!( 57 | "XDG_RUNTIME_DIR Variable is not set, falling back on hardcoded path." 58 | ); 59 | PathBuf::from(format!("/run/user/{}", unistd::Uid::current())) 60 | } 61 | _ => panic!("Unexpected error: {:#?}", e), 62 | }, 63 | }; 64 | 65 | Self { data_home, home, runtime_dir } 66 | } 67 | 68 | /// Actual interface to get the environment variable. 69 | fn get_env(name: &str) -> Result { 70 | match std::env::var(name) { 71 | Ok(val) => match PathBuf::from(&val).exists() { 72 | true => Ok(PathBuf::from(val)), 73 | false => Err(EnvError::PathNotFound), 74 | }, 75 | Err(e) => match e { 76 | VarError::NotPresent => match name { 77 | "XDG_DATA_HOME" => Err(EnvError::DataHomeNotSet), 78 | "HOME" => Err(EnvError::HomeNotSet), 79 | "XDG_RUNTIME_DIR" => Err(EnvError::RuntimeDirNotSet), 80 | _ => Err(EnvError::GenericError(format!("{} not set", name))), 81 | }, 82 | VarError::NotUnicode(_) => { 83 | Err(EnvError::GenericError(format!("{} not unicode", name))) 84 | } 85 | }, 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /swhks/src/ipc.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | hash::{DefaultHasher, Hash, Hasher}, 3 | io::{Read, Write}, 4 | os::unix::net::UnixListener, 5 | process::Command, 6 | }; 7 | 8 | /// Get the environment variables 9 | /// These would be requested from the default shell to make sure that the environment is up-to-date 10 | fn get_env() -> Result> { 11 | let shell = std::env::var("SHELL")?; 12 | let cmd = Command::new(shell).arg("-c").arg("env").output()?; 13 | let stdout = String::from_utf8(cmd.stdout)?; 14 | Ok(stdout) 15 | } 16 | 17 | /// Calculates a simple hash of the string 18 | /// Uses the DefaultHasher from the std::hash module which is not a cryptographically secure hash, 19 | /// however, it is good enough for our use case. 20 | pub fn calculate_hash(t: &str) -> u64 { 21 | let mut hasher = DefaultHasher::new(); 22 | t.hash(&mut hasher); 23 | hasher.finish() 24 | } 25 | 26 | pub fn server_loop(sock_file_path: &str) -> std::io::Result<()> { 27 | let mut prev_hash = calculate_hash(""); 28 | 29 | let listener = UnixListener::bind(sock_file_path)?; 30 | // Init a buffer to read the incoming message 31 | let mut buff = [0; 1]; 32 | log::debug!("Listening for incoming connections..."); 33 | 34 | for stream in listener.incoming() { 35 | match stream { 36 | Ok(mut stream) => { 37 | if let Err(e) = stream.read_exact(&mut buff) { 38 | log::error!("Failed to read from stream: {}", e); 39 | continue; 40 | } 41 | 42 | match buff[0] { 43 | 1 => { 44 | // If the buffer is [1] then it is a VERIFY message 45 | // the hash of the environment variables is sent back to the client 46 | // then the stream is flushed and the loop continues 47 | log::debug!("Received VERIFY request from swhkd"); 48 | if let Err(e) = stream.write_all(prev_hash.to_string().as_bytes()) { 49 | log::error!("Failed to write hash to stream: {}", e); 50 | } else { 51 | log::debug!("Sent hash to swhkd"); 52 | } 53 | } 54 | 2 => { 55 | // If the buffer is [2] then it is a GET message 56 | // the environment variables are sent back to the client 57 | // then the stream is flushed and the loop continues 58 | log::debug!("Received GET request from swhkd"); 59 | 60 | match get_env() { 61 | Ok(env) => { 62 | let new_hash = calculate_hash(&env); 63 | if prev_hash == new_hash { 64 | log::debug!("No changes in environment variables"); 65 | } else { 66 | log::debug!("Environment variables updated"); 67 | prev_hash = new_hash; 68 | } 69 | 70 | if let Err(e) = stream.write_all(env.as_bytes()) { 71 | log::error!("Failed to send environment variables: {}", e); 72 | } 73 | } 74 | Err(e) => { 75 | log::error!("Failed to retrieve environment variables: {}", e); 76 | let _ = stream.write_all(b"ERROR: Unable to fetch environment"); 77 | } 78 | } 79 | } 80 | _ => { 81 | log::warn!("Received unknown request: {}", buff[0]); 82 | } 83 | } 84 | 85 | if let Err(e) = stream.flush() { 86 | log::error!("Failed to flush stream: {}", e); 87 | } 88 | } 89 | Err(e) => { 90 | log::error!("Error handling connection: {}", e); 91 | break; 92 | } 93 | } 94 | } 95 | 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /swhks/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fs::Permissions; 3 | use std::{ 4 | fs::{self}, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use clap::Parser; 9 | use std::{ 10 | env, 11 | os::unix::fs::PermissionsExt, 12 | process::{exit, id}, 13 | }; 14 | use sysinfo::System; 15 | use sysinfo::{ProcessExt, SystemExt}; 16 | 17 | mod ipc; 18 | 19 | /// IPC Server for swhkd 20 | #[derive(Parser)] 21 | #[command(version, about, long_about = None)] 22 | struct Args { 23 | /// Enable Debug Mode 24 | #[arg(short, long)] 25 | debug: bool, 26 | } 27 | 28 | fn main() -> std::io::Result<()> { 29 | let args = Args::parse(); 30 | if args.debug { 31 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("swhks=trace")) 32 | .init(); 33 | } else { 34 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("swhks=warn")) 35 | .init(); 36 | } 37 | 38 | let invoking_uid = get_uid().unwrap(); 39 | let runtime_dir = format!("/run/user/{}", invoking_uid); 40 | 41 | let (_pid_file_path, sock_file_path) = get_file_paths(&runtime_dir); 42 | 43 | log::info!("Started SWHKS placeholder server"); 44 | 45 | // Daemonize the process 46 | let _ = nix::unistd::daemon(true, false); 47 | 48 | setup_swhks(invoking_uid, PathBuf::from(runtime_dir)); 49 | 50 | if Path::new(&sock_file_path).exists() { 51 | fs::remove_file(&sock_file_path)?; 52 | } 53 | 54 | ipc::server_loop(&sock_file_path)?; 55 | 56 | Ok(()) 57 | } 58 | 59 | pub fn setup_swhks(invoking_uid: u32, runtime_path: PathBuf) { 60 | // Get the runtime path and create it if needed. 61 | if !Path::new(&runtime_path).exists() { 62 | match fs::create_dir_all(Path::new(&runtime_path)) { 63 | Ok(_) => { 64 | log::debug!("Created runtime directory."); 65 | match fs::set_permissions(Path::new(&runtime_path), Permissions::from_mode(0o600)) { 66 | Ok(_) => log::debug!("Set runtime directory to readonly."), 67 | Err(e) => log::error!("Failed to set runtime directory to readonly: {}", e), 68 | } 69 | } 70 | Err(e) => log::error!("Failed to create runtime directory: {}", e), 71 | } 72 | } 73 | 74 | // Get the PID file path for instance tracking. 75 | let pidfile: String = format!("{}/swhks_{}.pid", runtime_path.to_string_lossy(), invoking_uid); 76 | if Path::new(&pidfile).exists() { 77 | log::trace!("Reading {} file and checking for running instances.", pidfile); 78 | let swhks_pid = match fs::read_to_string(&pidfile) { 79 | Ok(swhks_pid) => swhks_pid, 80 | Err(e) => { 81 | log::error!("Unable to read {} to check all running instances", e); 82 | exit(1); 83 | } 84 | }; 85 | log::debug!("Previous PID: {}", swhks_pid); 86 | 87 | // Check if swhkd is already running! 88 | let mut sys = System::new_all(); 89 | sys.refresh_all(); 90 | for (pid, process) in sys.processes() { 91 | if pid.to_string() == swhks_pid && process.exe() == env::current_exe().unwrap() { 92 | log::error!("Swhks is already running!"); 93 | log::error!("There is no need to run another instance since there is already one running with PID: {}", swhks_pid); 94 | exit(1); 95 | } 96 | } 97 | } 98 | 99 | // Write to the pid file. 100 | match fs::write(&pidfile, id().to_string()) { 101 | Ok(_) => {} 102 | Err(e) => { 103 | log::error!("Unable to write to {}: {}", pidfile, e); 104 | exit(1); 105 | } 106 | } 107 | } 108 | 109 | fn get_file_paths(runtime_dir: &str) -> (String, String) { 110 | let pid_file_path = format!("{}/swhks.pid", runtime_dir); 111 | let sock_file_path = format!("{}/swhkd.sock", runtime_dir); 112 | 113 | (pid_file_path, sock_file_path) 114 | } 115 | 116 | /// Get the UID of the user that is not a system user 117 | fn get_uid() -> Result> { 118 | let status_content = fs::read_to_string(format!("/proc/{}/loginuid", std::process::id()))?; 119 | let uid = status_content.trim().parse::()?; 120 | Ok(uid) 121 | } 122 | --------------------------------------------------------------------------------