├── .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 |
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 |
--------------------------------------------------------------------------------