├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── NOTES.md ├── README.md ├── gwm-core ├── Cargo.toml └── src │ ├── config.rs │ ├── layout │ └── mod.rs │ ├── lib.rs │ └── tree.rs ├── gwm-kbd ├── Cargo.toml ├── gwmkbdrc.toml └── src │ ├── kbd │ ├── config.rs │ ├── desc.rs │ ├── err.rs │ ├── mod.rs │ ├── modmask.rs │ └── state.rs │ ├── lib.rs │ └── main.rs ├── randr.txt ├── screenshot.png ├── xephyr └── xinitrc /.gitignore: -------------------------------------------------------------------------------- 1 | gwm-core/src-old 2 | target 3 | *_log 4 | *~ 5 | *.swp 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.3" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 9 | ] 10 | 11 | [[package]] 12 | name = "atty" 13 | version = "0.2.11" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | dependencies = [ 16 | "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 17 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 18 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 19 | ] 20 | 21 | [[package]] 22 | name = "bitflags" 23 | version = "0.9.1" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "0.1.7" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | 31 | [[package]] 32 | name = "env_logger" 33 | version = "0.6.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | dependencies = [ 36 | "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 37 | "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 38 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 39 | "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 40 | "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 41 | ] 42 | 43 | [[package]] 44 | name = "generational-arena" 45 | version = "0.2.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | dependencies = [ 48 | "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 49 | ] 50 | 51 | [[package]] 52 | name = "getopts" 53 | version = "0.2.18" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | dependencies = [ 56 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 57 | ] 58 | 59 | [[package]] 60 | name = "gwm-core" 61 | version = "0.8.0" 62 | dependencies = [ 63 | "generational-arena 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 64 | ] 65 | 66 | [[package]] 67 | name = "gwm-kbd" 68 | version = "0.1.0" 69 | dependencies = [ 70 | "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 71 | "getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", 72 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 73 | "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", 74 | "xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", 75 | "xkb 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 76 | ] 77 | 78 | [[package]] 79 | name = "humantime" 80 | version = "1.2.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | dependencies = [ 83 | "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 84 | ] 85 | 86 | [[package]] 87 | name = "lazy_static" 88 | version = "1.3.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | 91 | [[package]] 92 | name = "libc" 93 | version = "0.2.51" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | 96 | [[package]] 97 | name = "log" 98 | version = "0.4.6" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | dependencies = [ 101 | "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 102 | ] 103 | 104 | [[package]] 105 | name = "memchr" 106 | version = "2.2.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | 109 | [[package]] 110 | name = "pkg-config" 111 | version = "0.3.14" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | 114 | [[package]] 115 | name = "quick-error" 116 | version = "1.2.2" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | 119 | [[package]] 120 | name = "redox_syscall" 121 | version = "0.1.54" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | 124 | [[package]] 125 | name = "redox_termios" 126 | version = "0.1.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | dependencies = [ 129 | "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", 130 | ] 131 | 132 | [[package]] 133 | name = "regex" 134 | version = "1.1.6" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | dependencies = [ 137 | "aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", 138 | "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 139 | "regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", 140 | "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 141 | "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 142 | ] 143 | 144 | [[package]] 145 | name = "regex-syntax" 146 | version = "0.6.6" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | dependencies = [ 149 | "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 150 | ] 151 | 152 | [[package]] 153 | name = "serde" 154 | version = "1.0.90" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | 157 | [[package]] 158 | name = "termcolor" 159 | version = "1.0.4" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | dependencies = [ 162 | "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 163 | ] 164 | 165 | [[package]] 166 | name = "termion" 167 | version = "1.5.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | dependencies = [ 170 | "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 171 | "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", 172 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 173 | ] 174 | 175 | [[package]] 176 | name = "thread_local" 177 | version = "0.3.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | dependencies = [ 180 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 181 | ] 182 | 183 | [[package]] 184 | name = "toml" 185 | version = "0.4.10" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | dependencies = [ 188 | "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", 189 | ] 190 | 191 | [[package]] 192 | name = "ucd-util" 193 | version = "0.1.3" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | 196 | [[package]] 197 | name = "unicode-width" 198 | version = "0.1.5" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | 201 | [[package]] 202 | name = "utf8-ranges" 203 | version = "1.0.2" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | 206 | [[package]] 207 | name = "winapi" 208 | version = "0.3.7" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | dependencies = [ 211 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 212 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 213 | ] 214 | 215 | [[package]] 216 | name = "winapi-i686-pc-windows-gnu" 217 | version = "0.4.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | 220 | [[package]] 221 | name = "winapi-util" 222 | version = "0.1.2" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | dependencies = [ 225 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 226 | ] 227 | 228 | [[package]] 229 | name = "winapi-x86_64-pc-windows-gnu" 230 | version = "0.4.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | 233 | [[package]] 234 | name = "wincolor" 235 | version = "1.0.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | dependencies = [ 238 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 239 | "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 240 | ] 241 | 242 | [[package]] 243 | name = "xcb" 244 | version = "0.8.2" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | dependencies = [ 247 | "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 248 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 249 | ] 250 | 251 | [[package]] 252 | name = "xkb" 253 | version = "0.2.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | dependencies = [ 256 | "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 257 | "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 258 | "xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", 259 | "xkbcommon-sys 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", 260 | ] 261 | 262 | [[package]] 263 | name = "xkbcommon-sys" 264 | version = "0.7.4" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | dependencies = [ 267 | "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 268 | "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 269 | ] 270 | 271 | [metadata] 272 | "checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c" 273 | "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" 274 | "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" 275 | "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" 276 | "checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a" 277 | "checksum generational-arena 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4024f96ffa0ebaaf36aa589cd41f2fd69f3a5e6fd02c86e11e12cdf41d5b46a3" 278 | "checksum getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0a7292d30132fb5424b354f5dc02512a86e4c516fe544bb7a25e7f266951b797" 279 | "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" 280 | "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" 281 | "checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917" 282 | "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" 283 | "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" 284 | "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" 285 | "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" 286 | "checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" 287 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 288 | "checksum regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0a0bcab2fd7d1d7c54fa9eae6f43eddeb9ce2e7352f8518a814a4f65d60c58" 289 | "checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96" 290 | "checksum serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "aa5f7c20820475babd2c077c3ab5f8c77a31c15e16ea38687b4c02d3e48680f4" 291 | "checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f" 292 | "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 293 | "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" 294 | "checksum toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" 295 | "checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" 296 | "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" 297 | "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" 298 | "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" 299 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 300 | "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" 301 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 302 | "checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba" 303 | "checksum xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" 304 | "checksum xkb 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5113027c9bc322ab273d0004caee49dc962ac819e3916518f774331db2884ed" 305 | "checksum xkbcommon-sys 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fa434980dca02ebf28795d71e570dbb78316d095a228707efd6117bf8246d78b" 306 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "gwm-core", 4 | "gwm-kbd", 5 | ] 6 | 7 | [profile.release] 8 | opt-level = 3 9 | debug = false 10 | rpath = false 11 | lto = true 12 | debug-assertions = false 13 | codegen-units = 1 14 | panic = 'abort' 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Inokentiy Babushkin and contributors (c) 2016-2017 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * Neither the name of Inokentiy Babushkin nor the names of other 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | client: 2 | window (and maybe another unique id) 3 | geometry 4 | mapping 5 | properties 6 | tags 7 | 8 | tagset: 9 | unique id 10 | set of tags 11 | layout 12 | 13 | screen: 14 | unique id 15 | geometry 16 | 17 | tag_tree: 18 | container arena 19 | root container id 20 | focused container id 21 | selected/marked container id 22 | 23 | container: 24 | types: hsplit/vsplit/tab/window 25 | parent 26 | children 27 | last focused 28 | 29 | arena: 30 | map 31 | map 32 | map 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gabelstaplerwm 2 | **NOTE**: gabelstaplerwm is currently being rewritten (because I can't get my 3 | priorities straight and do such things). During that time, the `next` branch will hold 4 | the rewrite, the stable and working codebase will still receive bugfixes in case 5 | something catastrophic happens, and will be available in the `master` branch in the 6 | meantime. 7 | 8 | `gabelstaplerwm` is a semidynamic tiling window manager written in the Rust 9 | programming language and using the XCB wrappers available. It's main design 10 | goals are simplicity, correctness in behaviour and configurability, coupled 11 | with compile-time configuration by the user, minimizing overhead at runtime 12 | and allowing for sophisticated dynamic behaviour. This is achieved by minimizing 13 | the responsibilities of the window manager. 14 | 15 | ## Concepts 16 | `gabelstaplerwm` is inspired by `dwm` and `awesomewm`, adding a few ideas of it's own 17 | to the concept of a dynamic window manager. It shares the tag idiom with it's 18 | ancestors-in-spirit, but implements a slightly different approach at managing 19 | them. The author believes this is the most flexible solution achieved using 20 | such a system, while still efficient to implement and use. 21 | 22 | ### What are tags at all? 23 | Users of `awesome`/`dwm` are familiar with the tag concept. Where classical 24 | approaches to window management introduce a structure called a workspace, tags 25 | are a more flexible alternative. Essentially, each window is associated with 26 | a set of tags, and a user can display an arbitrary set of tags, which would 27 | render all windows tagged with at least one of those tags on the screen. 28 | 29 | Naturally, this concept is tightly coupled with layouts: if the sets of windows 30 | displayed on screen are computed dynamically, then manually arranging them in 31 | a grid would be painful and suboptimal. Thus, the geometries of the windows shown 32 | are computed dynamically by so-called layouts, which are simple arrangement 33 | patterns that can be applied to arbitrary numbers of windows. 34 | 35 | These concepts are battle-tested and implemented in a few so-called *dynamic* 36 | window managers, of which `awesome` and `dwm` are the most prominent. However, 37 | even if arbitrary sets of tags can be shown at any time, tags remain a very 38 | flexible implementation of workspaces. This is because tag sets with more than 39 | one element are hard to display in a quick fashion and remain second-class citizens. 40 | `gabelstaplerwm` reduces tags to symbolic values and allows for powerful 41 | mechanisms to add, remove, and edit tag sets at runtime, making working with the 42 | concept more efficient. 43 | 44 | ### How do you use them? 45 | At the time of writing, some changes are still being worked on, that might affect 46 | the recommended way of configuring the tag system. For now, see the code, and 47 | especially the `config` branch for the author's configuration. 48 | 49 | ### Why doesn't it have feature X? 50 | Frankly, the goal is to keep the window manager small and clean, yet functional, 51 | while avoiding feature creep. This means that no window decoration, no graphical 52 | effects, included status bars or wallpaper setting functionality is included. 53 | 54 | Those things are certainly useful, but can be provided from a different piece of 55 | software in a better way. 56 | 57 | ## How does it look? 58 | Tough question. As mentioned above, the visuals are pretty boring. Here is a 59 | screenshot of my configuration: 60 | 61 | ![screen](screenshot.png) 62 | 63 | The status bar is provided using [`lemonbar`](https://github.com/LemonBoy/bar) and 64 | [`bartender`](https://github.com/ibabushkin/bartender). Some of the inputs (particularly 65 | the currently shown tagset and window manager mode are provided by `gabelstaplerwm` 66 | itself. The visible menu is just a dmenu instance. 67 | 68 | ## Configuration and Installation 69 | Simple as the source itself: 70 | 71 | 1. Read the `src/wm/config.rs` file. 72 | 2. Read the other sources, as you see fit. I can also recommend the `config` branch 73 | for this purpose. Note that it interacts with other components I have set up. 74 | 3. Edit the configuration to your liking. 75 | 4. Compile and install with `cargo`. 76 | 5. Repeat as necessary. 77 | 78 | ## Documentation 79 | Currently, the only docs available are the (partly pretty extensive) comments in 80 | the sources. If there are unclear aspects, feel free to file an issue on GitHub. 81 | There is also a help document planned, but considering the configuration model, 82 | understanding the source is pretty useful anyway. 83 | 84 | ## Future Development 85 | The project isn't stale, but I pause it from time to time when other things happen. 86 | At the moment, I'm very happy with the set of features and focus mainly on bugfixes 87 | and performance improvements. 88 | 89 | There is also a long-term development underway that is supposed to bring `i3`-style 90 | window trees to `gabelstaplerwm`, ultimately yielding a combination of manual and 91 | dynamic layouts. 92 | 93 | ## Contributing 94 | Contribution is always welcome, be it bug reports, feedback, patches or proposals. 95 | GitHub should be an appropriate platform for this. 96 | -------------------------------------------------------------------------------- /gwm-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gwm-core" 3 | version = "0.8.0" 4 | authors = ["Inokentiy Babushkin "] 5 | description = "A semidynamic tiling window manager using XCB" 6 | documentation = "https://ibabushkin.github.io/rustdoc/gabelstaplerwm/" 7 | homepage = "https://ibabushkin.github.io/" 8 | repository = "https://github.com/ibabushkin/gabelstaplerwm" 9 | readme = "../README.md" 10 | keywords = ["xcb", "window manager", "X"] 11 | license = "BSD3" 12 | 13 | [dependencies] 14 | generational-arena = "^0.2" 15 | -------------------------------------------------------------------------------- /gwm-core/src/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Eq, Hash)] 2 | pub enum Tag { 3 | Work(i8), 4 | NonWork, 5 | } 6 | -------------------------------------------------------------------------------- /gwm-core/src/layout/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | #![allow(unused_variables)] 3 | #![allow(dead_code)] 4 | 5 | use std::collections::HashMap; 6 | use std::fmt::Debug; 7 | 8 | use tree::{ArenaContainerId, ContainerId, Container, SplitRatio, SplitType, TagTree}; 9 | 10 | /// A rectangle somewhere on screen. 11 | /// 12 | /// Could represent a client's geometry, a screen, or something else. 13 | #[derive(Copy, Clone)] 14 | pub struct Geometry { 15 | /// The x coordinate of the upper left corner of the rectangle. 16 | x: u32, 17 | /// The y coordinate of the upper left corner of the rectangle. 18 | y: u32, 19 | /// The width of the rectangle. 20 | width: u32, 21 | /// The height of the rectangle. 22 | height: u32, 23 | } 24 | 25 | impl Geometry { 26 | /// Split the given geometry horizontally in two. 27 | /// 28 | /// Return a pair of subgeometries (left first) computed in the split. 29 | pub fn split_horizontal(&self, ratio: SplitRatio) -> (Geometry, Geometry) { 30 | let width_prime = self.height * ratio; 31 | let x_prime = self.x + width_prime; 32 | 33 | let left = Geometry { 34 | x: self.x, 35 | y: self.y, 36 | width: width_prime, 37 | height: self.height, 38 | }; 39 | 40 | let right = Geometry { 41 | x: x_prime, 42 | y: self.y, 43 | width: self.width - width_prime, 44 | height: self.height, 45 | }; 46 | 47 | (left, right) 48 | } 49 | 50 | /// Split the given geometry vertically in two. 51 | /// 52 | /// Return a pair of subgeometries (top first) computed in the split. 53 | pub fn split_vertical(&self, ratio: SplitRatio) -> (Geometry, Geometry) { 54 | let height_prime = self.height * ratio; 55 | let y_prime = self.y + height_prime; 56 | 57 | let top = Geometry { 58 | x: self.x, 59 | y: self.y, 60 | width: self.width, 61 | height: height_prime, 62 | }; 63 | 64 | let bot = Geometry { 65 | x: self.x, 66 | y: y_prime, 67 | width: self.width, 68 | height: self.height - height_prime, 69 | }; 70 | 71 | (top, bot) 72 | } 73 | 74 | /// Split the given geometry horizontally in equal subgeometries. 75 | /// 76 | /// Returns the leftmost subgeometry, and an x-offset for each next geometry. 77 | pub fn split_horizontal_eq(&self, n: usize) -> (Geometry, u32) { 78 | let width_prime = self.width / n as u32; 79 | 80 | let left = Geometry { 81 | x: self.x, 82 | y: self.y, 83 | width: width_prime, 84 | height: self.height, 85 | }; 86 | 87 | (left, width_prime) 88 | } 89 | 90 | /// Split the given geometry vertically in equal subgeometries. 91 | /// 92 | /// Returns the topmost subgeometry, and an y-offset for each next geometry. 93 | pub fn split_vertical_eq(&self, n: usize) -> (Geometry, u32) { 94 | let height_prime = self.height / n as u32; 95 | 96 | let top = Geometry { 97 | x: self.x, 98 | y: self.y, 99 | width: self.width, 100 | height: height_prime, 101 | }; 102 | 103 | (top, height_prime) 104 | } 105 | 106 | /// Move the given geometry by the given offset in x direction. 107 | /// 108 | /// Returns the moved geometry. 109 | pub fn x_offset(&self, off_x: i32) -> Geometry { 110 | Geometry { 111 | x: (self.x as i32 + off_x) as u32, 112 | y: self.y, 113 | width: self.width, 114 | height: self.height, 115 | } 116 | } 117 | 118 | /// Move the given geometry by the given offset in y direction. 119 | /// 120 | /// Returns the moved geometry. 121 | pub fn y_offset(&self, off_y: i32) -> Geometry { 122 | Geometry { 123 | x: self.x, 124 | y: (self.y as i32 + off_y) as u32, 125 | width: self.width, 126 | height: self.height, 127 | } 128 | } 129 | 130 | pub fn offset(&self, split: &SplitType, off: i32) -> Geometry { 131 | match split { 132 | SplitType::Horizontal(_) => self.x_offset(off), 133 | SplitType::Vertical(_) => self.y_offset(off), 134 | SplitType::Tabbed => panic!("cannot offset geometry with tabbed split"), 135 | } 136 | } 137 | 138 | pub fn center(&mut self, reference: &Geometry) { 139 | self.x = reference.x + (reference.width / 2) - (self.width / 2); 140 | self.y = reference.y + (reference.height / 2) - (self.height / 2); 141 | } 142 | } 143 | 144 | /// Geometrical direction (in a tag tree). 145 | pub enum Direction { 146 | /// Geometric left (towards lower x-coordinates). 147 | Left, 148 | /// Geometric up (towards lower y-coordinates). 149 | Up, 150 | /// Geometric right (towards higher x-coordinates). 151 | Right, 152 | /// Geometric down (towards higher y-coordinates). 153 | Down, 154 | /// In-Order traversal, next element. 155 | InOrderForward, 156 | /// In-Order traversal, previous element. 157 | InOrderBackward, 158 | /// Pre-Order traversal, next element. 159 | PreOrderForward, 160 | /// Pre-Order traversal, previous element. 161 | PreOrderBackward, 162 | /// Sibling cycling, next sibling. 163 | SiblingCycleForward, 164 | /// Sibling cycling, previous sibling. 165 | SiblingCycleBackward, 166 | } 167 | 168 | /// A modification message sent to a layout. 169 | pub enum LayoutMessage { 170 | ParamAbs { id: usize, value: usize }, 171 | ParamAdd { id: usize, inc: usize }, 172 | } 173 | 174 | /// A map holding clients' geometries as constructed by a layout. 175 | pub type ClientSizes = HashMap; 176 | 177 | /// A layout that can be used to render tag trees on a geometry. 178 | /// 179 | /// Any layout type needs to uphold certain invariants to avoid surprising behaviour for 180 | /// the user. At the moment, this means that all clients displayed on a tagset need to be 181 | /// managed in the appropriate data structures. When containers are added, the layout needs to 182 | /// insert all client containers contained therein into the tree, and remove all clients from 183 | /// a container upon removal. All fields tracking focus and selection are not maintained by 184 | /// the layout. 185 | pub trait Layout : Debug { 186 | /// Compute geometries of the given tag tree on a given geometry. 187 | /// 188 | /// The tag tree can be assumed to be consistent with the layout. The layout can either 189 | /// ignore floating containers completely, or provide geometries for them that are then 190 | /// used in the actual rendering process. In either case, the floating windows and/or 191 | /// containers are then drawn at the provided or generated locations beginning at the root. 192 | fn render(&self, &TagTree, &Geometry, &mut ClientSizes); 193 | 194 | /// Check whether the tag tree is consistent with the layout. 195 | fn check_tree(&self, &TagTree) -> bool; 196 | 197 | /// Transform a tag tree to be consistent with the layout. 198 | fn fixup_tree(&self, &mut TagTree); 199 | 200 | /// Insert a new client into the tree and signify whether a new render is necessary. 201 | /// 202 | /// Arbitrary structural transformations of the tree to fit the layout are allowed, 203 | /// but the client must be inserted as a container. 204 | fn insert_client(&self, &mut TagTree, C) -> bool; 205 | 206 | /// Insert a copy of a container hierarchy into the tree and signify whether a new render 207 | /// is necessary. 208 | /// 209 | /// Arbitrary structural transformations of the tree to fit the layout are allowed, 210 | /// but the containers must be inserted. 211 | fn insert_container(&self, &mut TagTree, &TagTree, ContainerId) -> bool; 212 | 213 | /// Delete a container from the tree and signify whether a new render is necessary. 214 | /// 215 | /// The container can be assumed to be in the tree, and arbitrary transformations of 216 | /// the tree are allowed to fit the layout. However, the container must be removed. 217 | fn delete_container(&self, &mut TagTree, ContainerId) -> bool; 218 | 219 | /// Find an appropriate neighbour for a container located in the given direction. 220 | /// 221 | /// This is used to compute focus transitions and tree swap operations. In some cases, 222 | /// this can leave the tree in a state not consistent with the layout, which is then 223 | /// fixed using `fixup_tree`. 224 | fn find_container(&self, &TagTree, ContainerId, Direction) -> Option; 225 | 226 | /// Swap two containers in the tree, and signify whether a new render is necessary. 227 | /// 228 | /// The layout is allowed to not change the tree at all, or perform arbitrary structural 229 | /// updates on the tree. 230 | fn swap_containers(&self, &mut TagTree, ContainerId, ContainerId) -> bool; 231 | 232 | /// Move a container next to the cursor, and signify whether a new render is necessary. 233 | /// 234 | /// The layout is allowed to not change the tree at all, or perform arbitrary structural 235 | /// updates on the tree. 236 | fn move_container(&self, &mut TagTree, ContainerId, ContainerId) -> bool; 237 | 238 | /// Process a modification message and signify whether a new render is necessary. 239 | fn process_msg(&mut self, LayoutMessage) -> bool; 240 | } 241 | 242 | /// The manual layout. 243 | /// 244 | /// This layout essentially mirrors i3's approach to window management. The tag tree's 245 | /// contents are rendered directly, and can be of arbitrary structure. 246 | #[derive(Debug)] 247 | pub struct Manual { } 248 | 249 | impl Layout for Manual { 250 | fn render(&self, tagtree: &TagTree, target: &Geometry, sizes: &mut ClientSizes) { 251 | fn handle_split(tagtree: &TagTree, 252 | geo_cache: &mut HashMap, 253 | current_id: ContainerId, 254 | split_type: SplitType, 255 | last_focused: Option) 256 | { 257 | let num_children = tagtree.num_children(current_id); 258 | let (mut geo, offset) = match split_type { 259 | SplitType::Vertical(_) => { 260 | geo_cache[¤t_id].0.split_vertical_eq(num_children) 261 | }, 262 | SplitType::Horizontal(_) => { 263 | geo_cache[¤t_id].0.split_horizontal_eq(num_children) 264 | }, 265 | SplitType::Tabbed => { 266 | (geo_cache[¤t_id].0, 0) 267 | }, 268 | }; 269 | 270 | // handle hidden containers (the ones invisible in tabbed splits) 271 | let children_hidden = 272 | split_type != SplitType::Tabbed && geo_cache[¤t_id].1; 273 | 274 | for (child_id, child) in tagtree.children(current_id) { 275 | geo_cache.insert(ContainerId::Index(child_id), (geo, children_hidden)); 276 | geo = geo.offset(&split_type, offset as i32); 277 | } 278 | 279 | if let Some(l) = last_focused { 280 | geo_cache.get_mut(&ContainerId::Index(l)).unwrap().1 = 281 | geo_cache[¤t_id].1; 282 | } 283 | } 284 | 285 | // the geometry cache contains a geometry and a "will be actually rendered" flag. 286 | // this is needed to compute the geometries of hidden containers in tabbed splits 287 | // that are visible because they are floating 288 | let mut geo_cache = HashMap::with_capacity(tagtree.len()); 289 | geo_cache.insert(ContainerId::Root, (*target, true)); 290 | 291 | handle_split(tagtree, 292 | &mut geo_cache, 293 | ContainerId::Root, 294 | tagtree.root.split_type, 295 | tagtree.root.get_focused()); 296 | 297 | // loop invariant: at the beginning of each iteration, a geometry is cached for 298 | // the current container if it is to be drawn. 299 | for (current_id, current) in tagtree.preorder(ContainerId::Root) { 300 | let current_id = ContainerId::Index(current_id); 301 | 302 | // just move floating containers to the middle of the screen 303 | if current.floating() { 304 | geo_cache.get_mut(¤t_id).unwrap().0.center(target); 305 | } 306 | 307 | // since we are iterating over the preorder traversal of the tree, we can 308 | // maintain the invariant by caching geometries for the children of the current 309 | // container. 310 | match current { 311 | Container::Split(s) => { 312 | handle_split(tagtree, 313 | &mut geo_cache, 314 | current_id, 315 | s.split_type, 316 | s.get_last_focused()); 317 | }, 318 | Container::Client(c) => if geo_cache[¤t_id].1 { 319 | sizes.insert(current_id, geo_cache[¤t_id].0); 320 | }, 321 | } 322 | } 323 | } 324 | 325 | fn check_tree(&self, _: &TagTree) -> bool { true } 326 | 327 | fn fixup_tree(&self, _: &mut TagTree) { } 328 | 329 | fn insert_client(&self, tagtree: &mut TagTree, client: C) -> bool { 330 | if let Some(cursor) = tagtree.get_cursor() { 331 | tagtree.insert_client_after(cursor, client); 332 | } else { 333 | tagtree.insert_first_client(client); 334 | } 335 | 336 | false 337 | } 338 | 339 | fn insert_container(&self, tagtree: &mut TagTree, src: &TagTree, root: ContainerId) 340 | -> bool 341 | { 342 | // TODO 343 | false 344 | } 345 | 346 | fn delete_container(&self, tagtree: &mut TagTree, container: ContainerId) -> bool { 347 | tagtree.delete_container(container); 348 | 349 | // TODO: cleverly detect if a redraw is necessary. essentially, this requires some 350 | // intrusive handling of `last_focused` updates on tabbed containers. 351 | true 352 | } 353 | 354 | fn find_container(&self, tagtree: &TagTree, container: ContainerId, dir: Direction) 355 | -> Option 356 | { 357 | // TODO 358 | None 359 | } 360 | 361 | fn swap_containers(&self, 362 | tagtree: &mut TagTree, 363 | a: ContainerId, 364 | b: ContainerId) -> bool { 365 | // TODO 366 | false 367 | } 368 | 369 | fn move_container(&self, 370 | tagtree: &mut TagTree, 371 | cursor: ContainerId, 372 | target: ContainerId) -> bool { 373 | // TODO 374 | false 375 | } 376 | 377 | fn process_msg(&mut self, _: LayoutMessage) -> bool { false } 378 | } 379 | -------------------------------------------------------------------------------- /gwm-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_enum_variants)] 2 | extern crate generational_arena; 3 | 4 | pub mod config; 5 | pub mod layout; 6 | pub mod tree; 7 | -------------------------------------------------------------------------------- /gwm-core/src/tree.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | #![allow(unused_variables)] 3 | #![allow(dead_code)] 4 | 5 | use std::collections::{BTreeSet, HashMap, HashSet}; 6 | use std::ops::{Add, Sub, Mul}; 7 | 8 | use config::Tag; 9 | use layout::{Geometry, Layout}; 10 | 11 | use generational_arena::Arena; 12 | pub use generational_arena::Index as ArenaId; 13 | 14 | pub type ArenaContainerId = ArenaId; 15 | 16 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 17 | pub enum ContainerId { 18 | Root, 19 | Index(ArenaContainerId), 20 | } 21 | 22 | pub struct Client { 23 | id: C, 24 | currently_mapped: bool, 25 | properties: (), 26 | tags: HashSet, 27 | } 28 | 29 | pub struct ClientHierarchy { 30 | screens: Vec, 31 | tagsets: Arena>, 32 | clients: HashMap>, 33 | } 34 | 35 | pub type TagSetId = ArenaId; 36 | 37 | pub struct Screen { 38 | geometry: Geometry, 39 | tagset: TagSetId, 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct TagSet { 44 | tags: BTreeSet, 45 | tree: TagTree, 46 | layout: Box>, 47 | } 48 | 49 | // A tag tree. 50 | // 51 | // Represents the structure of clients that are tagged with a set of tags and displayed using 52 | // a given layout. The existance of multiple tag trees with identical sets of clients is allowed. 53 | // However, if the layout used to display them is identical as well, the use of such duplication 54 | // is questionable. 55 | // 56 | // All clients and split containers in a tag tree are stored in the same arena. This means that 57 | // containers from different tag trees are not directly accessible. 58 | #[derive(Debug)] 59 | pub struct TagTree { 60 | /// The root node of the tag tree, representing the outermost split. 61 | pub root: TagTreeContainer, 62 | /// The arena of containers in the tag tree. 63 | containers: Arena>, 64 | } 65 | 66 | impl TagTree { 67 | /// Create a new tag tree with the given root split type. 68 | pub fn new(root_split: SplitType) -> Self { 69 | let containers = Arena::new(); 70 | let root = TagTreeContainer::new(root_split); 71 | 72 | TagTree { 73 | containers, 74 | root, 75 | } 76 | } 77 | 78 | pub fn get_cursor(&self) -> Option { 79 | self.root.selected.or(self.root.focused) 80 | } 81 | 82 | pub fn insert_first_client(&mut self, client: C) -> ArenaContainerId { 83 | assert!(self.root.children.is_none()); 84 | 85 | let container = ClientContainer::new(client, ContainerId::Root); 86 | let id = self.containers.insert(Container::Client(container)); 87 | 88 | self.root.set_initial_child(id); 89 | 90 | id 91 | } 92 | 93 | /// Insert a client as a sibling before the cursor. 94 | /// 95 | /// Returns the inserted container. Panics if the cursor is orphaned. 96 | pub fn insert_client_before(&mut self, cursor: ArenaContainerId, client: C) 97 | -> ArenaContainerId 98 | { 99 | let parent = self.containers[cursor].get_parent().expect("cursor is orphaned"); 100 | let mut container = ClientContainer::new(client, parent); 101 | 102 | container.next_sibling = Some(cursor); 103 | 104 | let id = self.containers.insert(Container::Client(container)); 105 | 106 | self.containers[cursor].set_prev_sibling(Some(id)); 107 | 108 | if let Some(prev) = self.containers[cursor].get_prev_sibling() { 109 | self.containers[id].set_prev_sibling(Some(prev)); 110 | self.containers[prev].set_next_sibling(Some(id)); 111 | } else { 112 | match parent { 113 | ContainerId::Root => self.root.set_first_child(id), 114 | ContainerId::Index(p) => self.containers[p].set_first_child(id), 115 | } 116 | } 117 | 118 | id 119 | } 120 | 121 | /// Insert a client as a sibling after the cursor. 122 | /// 123 | /// Returns the inserted container. 124 | pub fn insert_client_after(&mut self, cursor: ArenaContainerId, client: C) 125 | -> ArenaContainerId 126 | { 127 | let parent = self.containers[cursor].get_parent().expect("cursor is orphaned"); 128 | let mut container = ClientContainer::new(client, parent); 129 | 130 | container.prev_sibling = Some(cursor); 131 | 132 | let id = self.containers.insert(Container::Client(container)); 133 | 134 | self.containers[cursor].set_next_sibling(Some(id)); 135 | 136 | if let Some(next) = self.containers[cursor].get_next_sibling() { 137 | self.containers[id].set_next_sibling(Some(next)); 138 | self.containers[next].set_prev_sibling(Some(id)); 139 | } else { 140 | match parent { 141 | ContainerId::Root => self.root.set_last_child(id), 142 | ContainerId::Index(p) => self.containers[p].set_last_child(id), 143 | } 144 | } 145 | 146 | id 147 | } 148 | 149 | /// Move a subtree as a sibling before the cursor. 150 | /// 151 | /// If the subtree is not orphaned, a check is performed whether the cursor is one of its 152 | /// descendants. If so, nothing is done and `false` returned. Otherwise, the subtree is 153 | /// reparented properly. If it was orphaned, it is just inserted before the cursor. In both 154 | /// cases, `true` is returned. 155 | pub fn move_subtree_before(&mut self, cursor: ArenaContainerId, tree: ArenaContainerId) 156 | -> bool 157 | { 158 | if cursor == tree { 159 | return false; 160 | } 161 | 162 | for (id, _) in self.preorder(ContainerId::Index(tree)) { 163 | if id == cursor { 164 | return false; 165 | } 166 | } 167 | 168 | self.containers[cursor].set_prev_sibling(Some(tree)); 169 | self.containers[tree].set_next_sibling(Some(cursor)); 170 | 171 | if let Some(prev) = self.containers[cursor].get_prev_sibling() { 172 | self.containers[tree].set_prev_sibling(Some(prev)); 173 | self.containers[prev].set_next_sibling(Some(tree)); 174 | } else { 175 | self.containers[tree].set_prev_sibling(None); 176 | 177 | match self.containers[cursor].get_parent().expect("cursor is orphaned") { 178 | ContainerId::Root => self.root.set_first_child(tree), 179 | ContainerId::Index(p) => self.containers[p].set_first_child(tree), 180 | } 181 | } 182 | 183 | true 184 | } 185 | 186 | /// Move a subtree as a sibling after the cursor. 187 | /// 188 | /// If the subtree is not orphaned, a check is performed whether the cursor is one of its 189 | /// descendants. If so, nothing is done and `false` returned. Otherwise, the subtree is 190 | /// reparented properly. If it was orphaned, it is just inserted after the cursor. In both 191 | /// cases, `true` is returned. 192 | pub fn move_subtree_after(&mut self, cursor: ArenaContainerId, tree: ArenaContainerId) 193 | -> bool 194 | { 195 | if cursor == tree { 196 | return false; 197 | } 198 | 199 | for (id, _) in self.preorder(ContainerId::Index(tree)) { 200 | if id == cursor { 201 | return false; 202 | } 203 | } 204 | 205 | self.containers[cursor].set_next_sibling(Some(tree)); 206 | self.containers[tree].set_prev_sibling(Some(cursor)); 207 | 208 | if let Some(next) = self.containers[cursor].get_next_sibling() { 209 | self.containers[tree].set_next_sibling(Some(next)); 210 | self.containers[next].set_prev_sibling(Some(tree)); 211 | } else { 212 | self.containers[tree].set_next_sibling(None); 213 | 214 | match self.containers[cursor].get_parent().expect("cursor is orphaned") { 215 | ContainerId::Root => self.root.set_last_child(tree), 216 | ContainerId::Index(p) => self.containers[p].set_last_child(tree), 217 | } 218 | } 219 | 220 | true 221 | } 222 | 223 | /// Construct a copy of the foreign subtree in the local arena and insert the subtree before 224 | /// the cursor. 225 | /// 226 | /// Returns `None` if `other == self`, otherwise the container id of the root of the new 227 | /// subtree. 228 | pub fn insert_foreign_subtree_before(&mut self, cursor: ArenaContainerId, 229 | other: &Self, subtree: ContainerId) 230 | -> Option 231 | { 232 | unimplemented!() 233 | } 234 | 235 | /// Construct a copy of the foreign subtree in the local arena and insert the subtree after 236 | /// the cursor. 237 | /// 238 | /// Returns `None` if `other == self`, otherwise the container id of the root of the new 239 | /// subtree. 240 | pub fn insert_foreign_subtree_after(&mut self, cursor: ArenaContainerId, 241 | other: &Self, subtree: ContainerId) 242 | -> Option 243 | { 244 | unimplemented!() 245 | } 246 | 247 | /// Insert a split container as the parent of the given cursor. 248 | /// 249 | /// Returns the id of the newly inserted container. 250 | pub fn split_container(&mut self, cursor: ArenaContainerId, dir: SplitType) 251 | -> ArenaContainerId 252 | { 253 | let parent = self.containers[cursor].get_parent().expect("cursor is orphaned"); 254 | let container = SplitContainer::new(dir, (cursor, cursor)); 255 | let id = self.containers.insert(Container::Split(container)); 256 | 257 | let (split, child) = self.containers.get2_mut(id, cursor); 258 | split.unwrap().swap_siblings(child.unwrap()); 259 | 260 | match parent { 261 | ContainerId::Root => self.root.update_children(cursor, id), 262 | ContainerId::Index(i) => self.containers[i].update_children(cursor, id), 263 | } 264 | 265 | id 266 | } 267 | 268 | pub fn delete_container(&mut self, cursor: ContainerId) { 269 | let mut cursor = match cursor { 270 | ContainerId::Root => { 271 | self.root.reset(); 272 | self.containers.clear(); 273 | 274 | return; 275 | }, 276 | ContainerId::Index(i) => i, 277 | }; 278 | 279 | while let Some(parent) = self.containers[cursor].get_parent() { 280 | if let Some(prev) = self.containers[cursor].get_prev_sibling() { 281 | let succ = self.containers[cursor].get_next_sibling(); 282 | self.containers[prev].set_next_sibling(succ); 283 | 284 | match parent { 285 | ContainerId::Root => 286 | self.root.update_last_child(cursor, prev), 287 | ContainerId::Index(p) => 288 | self.containers[p].update_last_child(cursor, prev), 289 | } 290 | } 291 | 292 | if let Some(next) = self.containers[cursor].get_next_sibling() { 293 | let pred = self.containers[cursor].get_prev_sibling(); 294 | self.containers[next].set_next_sibling(pred); 295 | 296 | match parent { 297 | ContainerId::Root => 298 | self.root.update_first_child(cursor, next), 299 | ContainerId::Index(p) => 300 | self.containers[p].update_first_child(cursor, next), 301 | } 302 | } 303 | 304 | self.containers.remove(cursor); 305 | 306 | match parent { 307 | ContainerId::Index(i) if self.num_children(parent) == 1 => 308 | cursor = i, 309 | _ => break, 310 | } 311 | } 312 | } 313 | 314 | pub fn preorder(&self, id: ContainerId) -> TagTreePreorder { 315 | TagTreePreorder { 316 | tree: self, 317 | root: id, 318 | current: id, 319 | } 320 | } 321 | 322 | pub fn children(&self, id: ContainerId) -> TagTreeChildren { 323 | let current = match id { 324 | ContainerId::Root => self.root.get_children(), 325 | ContainerId::Index(i) => self.containers[i].get_children(), 326 | }.map(|c| c.0); 327 | 328 | TagTreeChildren { 329 | tree: self, 330 | current, 331 | } 332 | } 333 | 334 | pub fn len(&self) -> usize { 335 | self.containers.len() 336 | } 337 | 338 | pub fn is_empty(&self) -> bool { 339 | self.containers.is_empty() 340 | } 341 | 342 | pub fn num_children(&self, id: ContainerId) -> usize { 343 | self.children(id).len() 344 | } 345 | } 346 | 347 | pub struct TagTreeChildren<'a, C> { 348 | tree: &'a TagTree, 349 | current: Option, 350 | } 351 | 352 | impl<'a, C> Iterator for TagTreeChildren<'a, C> { 353 | type Item = (ArenaContainerId, &'a Container); 354 | 355 | fn next(&mut self) -> Option { 356 | self.current 357 | .and_then(|i| self.tree.containers[i].get_next_sibling()) 358 | .map(|n| (n, &self.tree.containers[n])) 359 | } 360 | } 361 | 362 | impl<'a, C> ExactSizeIterator for TagTreeChildren<'a, C> { } 363 | 364 | pub struct TagTreePreorder<'a, C> { 365 | tree: &'a TagTree, 366 | root: ContainerId, 367 | current: ContainerId, 368 | } 369 | 370 | impl<'a, C> Iterator for TagTreePreorder<'a, C> { 371 | type Item = (ArenaContainerId, &'a Container); 372 | 373 | fn next(&mut self) -> Option { 374 | match self.current { 375 | ContainerId::Root => { 376 | if let Some((i, _)) = self.tree.root.get_children() { 377 | self.current = ContainerId::Index(i); 378 | Some((i, &self.tree.containers[i])) 379 | } else { 380 | None 381 | } 382 | }, 383 | ContainerId::Index(current) => { 384 | let c = &self.tree.containers[current]; 385 | 386 | if let Some(i) = c.get_children().map(|c| c.0).or_else(|| c.get_next_sibling()) { 387 | self.current = ContainerId::Index(i); 388 | Some((i, &self.tree.containers[i])) 389 | } else { 390 | while let Some(ContainerId::Index(i)) = 391 | self.tree.containers[current].get_parent() 392 | { 393 | if ContainerId::Index(i) == self.root { 394 | break; 395 | } 396 | 397 | self.current = ContainerId::Index(i); 398 | 399 | if let Some(n) = self.tree.containers[i].get_next_sibling() { 400 | return Some((n, &self.tree.containers[n])); 401 | } 402 | } 403 | 404 | None 405 | } 406 | } 407 | } 408 | } 409 | } 410 | 411 | /// A tag tree's root container. 412 | /// 413 | /// Exists for the duration of the tag tree's lifetime. This gives us the nice property that 414 | /// the tag trees always have at least one node, which can be identified because it has no 415 | /// parent (and using its type ;). Also holds information on focus and selection markers, as 416 | /// the last focused client below the root is the currently focused one. 417 | #[derive(Debug)] 418 | pub struct TagTreeContainer { 419 | /// The split type at the root. 420 | pub split_type: SplitType, 421 | /// The currently focused client. 422 | focused: Option, 423 | /// The currently selected container. 424 | selected: Option, 425 | /// First and last child of the root node, if any. 426 | children: Option<(ArenaContainerId, ArenaContainerId)>, 427 | } 428 | 429 | impl TagTreeContainer { 430 | /// Construct a new tag tree container given the split type it should use. 431 | fn new(split_type: SplitType) -> Self { 432 | TagTreeContainer { 433 | split_type, 434 | focused: None, 435 | selected: None, 436 | children: None, 437 | } 438 | } 439 | 440 | fn reset(&mut self) { 441 | self.focused = None; 442 | self.selected = None; 443 | self.children = None; 444 | } 445 | 446 | pub fn get_children(&self) -> Option<(ArenaContainerId, ArenaContainerId)> { 447 | self.children 448 | } 449 | 450 | fn set_initial_child(&mut self, child: ArenaContainerId) { 451 | if self.children.is_none() { 452 | self.focused = Some(child); 453 | self.selected = None; 454 | self.children = Some((child, child)); 455 | } else { 456 | panic!("Attempted to reinit children of non-empty tag tree container"); 457 | } 458 | } 459 | 460 | fn set_first_child(&mut self, child: ArenaContainerId) { 461 | match self.children { 462 | Some(ref mut c) => c.0 = child, 463 | None => panic!("Attempted to update single child of empty tag tree container"), 464 | } 465 | } 466 | 467 | fn set_last_child(&mut self, child: ArenaContainerId) { 468 | match self.children { 469 | Some(ref mut c) => c.1 = child, 470 | None => panic!("Attempted to update single child of empty tag tree container"), 471 | } 472 | } 473 | 474 | fn update_first_child(&mut self, old: ArenaContainerId, new: ArenaContainerId) { 475 | if let Some(ref mut c) = self.children { 476 | if c.0 == old { 477 | c.0 = new; 478 | } 479 | } 480 | } 481 | 482 | fn update_last_child(&mut self, old: ArenaContainerId, new: ArenaContainerId) { 483 | if let Some(ref mut c) = self.children { 484 | if c.1 == old { 485 | c.1 = new; 486 | } 487 | } 488 | } 489 | 490 | fn update_children(&mut self, old: ArenaContainerId, new: ArenaContainerId) { 491 | if let Some(ref mut c) = self.children { 492 | if c.0 == old { 493 | c.0 = new; 494 | } 495 | 496 | if c.1 == old { 497 | c.1 = new; 498 | } 499 | } 500 | } 501 | 502 | pub fn get_focused(&self) -> Option { 503 | self.focused 504 | } 505 | } 506 | 507 | /// A container is a node in a tag tree. 508 | /// 509 | /// Can be either a split container (which is always an inner node), or a client container (which 510 | /// is always a leaf). 511 | #[derive(Debug)] 512 | pub enum Container { 513 | /// A split container. 514 | Split(SplitContainer), 515 | /// A client container. 516 | Client(ClientContainer), 517 | } 518 | 519 | impl Container { 520 | pub fn floating(&self) -> bool { 521 | match self { 522 | Self::Split(s) => s.floating, 523 | Self::Client(c) => c.floating, 524 | } 525 | } 526 | 527 | pub fn last_focused(&self) -> Option { 528 | match self { 529 | Self::Split(s) => s.last_focused, 530 | Self::Client(c) => None, 531 | } 532 | } 533 | 534 | pub fn get_parent(&self) -> Option { 535 | match self { 536 | Self::Split(s) => s.parent, 537 | Self::Client(c) => c.parent, 538 | } 539 | } 540 | 541 | fn set_parent(&mut self, parent: Option) { 542 | match self { 543 | Self::Split(s) => s.parent = parent, 544 | Self::Client(c) => c.parent = parent, 545 | } 546 | } 547 | 548 | pub fn get_children(&self) -> Option<(ArenaContainerId, ArenaContainerId)> { 549 | match self { 550 | Self::Split(s) => Some(s.children), 551 | _ => None, 552 | } 553 | } 554 | 555 | pub fn set_first_child(&mut self, child: ArenaContainerId) { 556 | match self { 557 | Self::Split(s) => s.children.0 = child, 558 | _ => panic!("attempted to set child of client container"), 559 | } 560 | } 561 | 562 | pub fn set_last_child(&mut self, child: ArenaContainerId) { 563 | match self { 564 | Self::Split(s) => s.children.1 = child, 565 | _ => panic!("attempted to set child of client container"), 566 | } 567 | } 568 | 569 | fn update_first_child(&mut self, old: ArenaContainerId, new: ArenaContainerId) { 570 | match self { 571 | Self::Split(ref mut s) => { 572 | if s.children.0 == old { 573 | s.children.0 = new; 574 | } 575 | }, 576 | _ => panic!("attempted to update children of client container"), 577 | } 578 | } 579 | 580 | fn update_last_child(&mut self, old: ArenaContainerId, new: ArenaContainerId) { 581 | match self { 582 | Self::Split(ref mut s) => { 583 | if s.children.1 == old { 584 | s.children.1 = new; 585 | } 586 | }, 587 | _ => panic!("attempted to update children of client container"), 588 | } 589 | } 590 | 591 | fn update_children(&mut self, old: ArenaContainerId, new: ArenaContainerId) { 592 | match self { 593 | Self::Split(ref mut s) => { 594 | if s.children.0 == old { 595 | s.children.0 = new; 596 | } 597 | 598 | if s.children.1 == old { 599 | s.children.1 = new; 600 | } 601 | }, 602 | _ => panic!("attempted to update children of client container"), 603 | } 604 | } 605 | 606 | fn get_prev_sibling(&self) -> Option { 607 | match self { 608 | Self::Split(s) => s.prev_sibling, 609 | Self::Client(c) => c.prev_sibling, 610 | } 611 | } 612 | 613 | fn set_prev_sibling(&mut self, id: Option) { 614 | match self { 615 | Self::Split(s) => s.prev_sibling = id, 616 | Self::Client(c) => c.prev_sibling = id, 617 | } 618 | } 619 | 620 | fn get_next_sibling(&self) -> Option { 621 | match self { 622 | Self::Split(s) => s.next_sibling, 623 | Self::Client(c) => c.next_sibling, 624 | } 625 | } 626 | 627 | fn set_next_sibling(&mut self, id: Option) { 628 | match self { 629 | Self::Split(s) => s.next_sibling = id, 630 | Self::Client(c) => c.next_sibling = id, 631 | } 632 | } 633 | 634 | fn swap_siblings(&mut self, other: &mut Self) { 635 | use std::mem; 636 | 637 | match (self, other) { 638 | (Self::Split(s1), Self::Split(s2)) => { 639 | mem::swap(&mut s1.prev_sibling, &mut s2.prev_sibling); 640 | mem::swap(&mut s1.next_sibling, &mut s2.next_sibling); 641 | }, 642 | (Self::Split(s1), Self::Client(c2)) => { 643 | mem::swap(&mut s1.prev_sibling, &mut c2.prev_sibling); 644 | mem::swap(&mut s1.next_sibling, &mut c2.next_sibling); 645 | }, 646 | (Self::Client(c1), Self::Split(s2)) => { 647 | mem::swap(&mut c1.prev_sibling, &mut s2.prev_sibling); 648 | mem::swap(&mut c1.next_sibling, &mut s2.next_sibling); 649 | }, 650 | (Self::Client(c1), Self::Client(c2)) => { 651 | mem::swap(&mut c1.prev_sibling, &mut c2.prev_sibling); 652 | mem::swap(&mut c1.next_sibling, &mut c2.next_sibling); 653 | }, 654 | } 655 | } 656 | } 657 | 658 | /// A split container is an inner node in a tag tree. 659 | /// 660 | /// Always has a parent, as the root is a different type of container, otherwise considered 661 | /// invalid, invalid trees are cleaned up by the implementation if necessary. We also maintain 662 | /// the invariant that dangling split containers (that is, split containers without any 663 | /// children) may not exist. Thus, we force the presence of children. 664 | #[derive(Debug)] 665 | pub struct SplitContainer { 666 | /// The container's split type. 667 | pub split_type: SplitType, 668 | /// Whether the entire container is floating. 669 | pub floating: bool, 670 | /// the last descendant client container focused. 671 | last_focused: Option, 672 | /// The children of the split (first and last child). 673 | /// 674 | /// We do not allow split containers without children (they create nasty edge cases). 675 | children: (ArenaContainerId, ArenaContainerId), 676 | /// The parent of the container. 677 | /// 678 | /// If `None`, the subtree rooted by the container is considered dangling and no longer 679 | /// used. This means that it can not be assumed to be retained by the implementation if it 680 | /// still exists when a layout is done with its transformation for example. 681 | parent: Option, 682 | /// The previous sibling of the container, if any. 683 | prev_sibling: Option, 684 | /// The next sibling of the container, if any. 685 | next_sibling: Option, 686 | } 687 | 688 | impl SplitContainer { 689 | fn new(split_type: SplitType, children: (ArenaContainerId, ArenaContainerId)) -> Self { 690 | SplitContainer { 691 | split_type, 692 | last_focused: None, 693 | floating: false, 694 | children, 695 | parent: None, 696 | prev_sibling: None, 697 | next_sibling: None, 698 | } 699 | } 700 | 701 | fn is_orphaned(&self) -> bool { 702 | self.parent.is_none() 703 | } 704 | 705 | pub fn get_last_focused(&self) -> Option { 706 | self.last_focused 707 | } 708 | } 709 | 710 | /// A client container is a leaf in a tag tree. 711 | /// 712 | /// Always has a parent, as the root is a different type of container, otherwise considered 713 | /// invalid, invalid trees are cleaned up by the implementation if necessary. 714 | #[derive(Debug)] 715 | pub struct ClientContainer { 716 | /// Whether the client is floating. 717 | pub floating: bool, 718 | /// The client information. 719 | client: C, 720 | /// The parent of the container. 721 | /// 722 | /// If `None`, the subtree rooted by the container is considered dangling and no longer 723 | /// used. This means that it can not be assumed to be retained by the implementation if it 724 | /// still exists when a layout is done with its transformation for example. 725 | parent: Option, 726 | /// The previous sibling of the container, if any. 727 | prev_sibling: Option, 728 | /// The next sibling of the container, if any. 729 | next_sibling: Option, 730 | } 731 | 732 | impl ClientContainer { 733 | fn new(client: C, parent: ContainerId) -> Self { 734 | ClientContainer { 735 | floating: false, 736 | client, 737 | parent: Some(parent), 738 | prev_sibling: None, 739 | next_sibling: None, 740 | } 741 | } 742 | } 743 | 744 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 745 | pub struct SplitRatio(u8); 746 | 747 | impl SplitRatio { 748 | fn new(inner: u8) -> Self { 749 | use std::cmp::max; 750 | 751 | SplitRatio(max(inner, 100)) 752 | } 753 | } 754 | 755 | impl Sub for SplitRatio { 756 | type Output = SplitRatio; 757 | 758 | fn sub(self, rhs: u8) -> Self::Output { 759 | SplitRatio(self.0.saturating_sub(rhs)) 760 | } 761 | } 762 | 763 | impl Add for SplitRatio { 764 | type Output = SplitRatio; 765 | 766 | fn add(self, rhs: u8) -> Self::Output { 767 | use std::cmp::max; 768 | 769 | SplitRatio(max(self.0 + rhs, 100)) 770 | } 771 | } 772 | 773 | impl Mul for u32 { 774 | type Output = u32; 775 | 776 | fn mul(self, rhs: SplitRatio) -> Self::Output { 777 | ((self as usize) * 100 / rhs.0 as usize) as u32 778 | } 779 | } 780 | 781 | // Split ratios are not always senseful, as split containers can have more than two children.. 782 | // In such cases, multiple approaches can be taken by a layout: either ignoring ratios 783 | // altogether, forcing the split container to contain only two children, or somehow honoring the 784 | // ratio either once or recursively across the sequence of children. 785 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 786 | pub enum SplitType { 787 | Horizontal(SplitRatio), 788 | Vertical(SplitRatio), 789 | Tabbed, 790 | } 791 | -------------------------------------------------------------------------------- /gwm-kbd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gwm-kbd" 3 | version = "0.1.0" 4 | authors = ["Inokentiy Babushkin "] 5 | description = "A keybinding deamon for gabelstaplerwm." 6 | documentation = "https://ibabushkin.github.io/rustdoc/gabelstaplerwm/" 7 | homepage = "https://ibabushkin.github.io/" 8 | repository = "https://github.com/ibabushkin/gabelstaplerwm" 9 | readme = "../README.md" 10 | keywords = ["xcb", "window manager", "X"] 11 | license = "BSD3" 12 | 13 | [dependencies] 14 | # libc = "^0.2" 15 | env_logger = "^0.6" 16 | getopts = "^0.2.15" 17 | log = "^0.4" 18 | toml = "^0.4.5" 19 | xcb = { version = "^0.8", features = ["xkb"] } 20 | xkb = { version = "^0.2", features = ["x11"] } 21 | # pledge = { version = "*", optional = true } 22 | -------------------------------------------------------------------------------- /gwm-kbd/gwmkbdrc.toml: -------------------------------------------------------------------------------- 1 | modkey = "mod1" 2 | timeout = 400 3 | 4 | active_modes = [ 5 | "normal", # first mode listed is default 6 | "swap", 7 | "move", 8 | "ignore" 9 | ] 10 | 11 | [modes.normal] 12 | enter_binding = "$modkey+n" 13 | enter_binding_quick_leave = "$modkey+m $modkey+n" 14 | enter_cmd = "echo 'n'" 15 | leave_cmd = "echo '!n'" 16 | 17 | [modes.normal.bindings] 18 | "$modkey+x $modkey+h" = "echo xh" 19 | "$modkey+x $modkey+j" = "echo xj" 20 | "$modkey+x $modkey+k" = "echo xk" 21 | "$modkey+x $modkey+l" = "echo xl" 22 | "$modkey+h" = "echo h" 23 | "$modkey+j" = "echo j" 24 | "$modkey+k" = "echo k" 25 | "$modkey+l" = "echo l" 26 | "$modkey+shift+h" = "echo H" 27 | "$modkey+shift+j" = "echo J" 28 | "$modkey+shift+k" = "echo K" 29 | "$modkey+shift+l" = "echo L" 30 | 31 | [modes.swap] 32 | enter_binding = "$modkey+s" 33 | enter_binding_quick_leave = "$modkey+m $modkey+s" 34 | 35 | [modes.swap.bindings] 36 | "$modkey+j" = "echo j-s" 37 | 38 | [modes.move] 39 | enter_binding = "$modkey+shift+m" 40 | enter_binding_quick_leave = "$modkey+m $modkey+m" 41 | bindings = {} 42 | 43 | [modes.ignore] 44 | enter_binding = "$modkey+i" 45 | enter_binding_quick_leave = "$modkey+m $modkey+i" 46 | bindings = {} 47 | -------------------------------------------------------------------------------- /gwm-kbd/src/kbd/config.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | use std::fs::File; 36 | use std::io::prelude::*; 37 | use std::path::Path; 38 | 39 | use toml::value::{Array, Table, Value}; 40 | 41 | use kbd::err::*; 42 | 43 | /// Try to parse a TOML table from a config file, given as a path. 44 | pub fn parse_file(path: &Path) -> KbdResult { 45 | match File::open(path) { 46 | Ok(mut file) => { 47 | let mut toml_str = String::new(); 48 | 49 | match file.read_to_string(&mut toml_str) { 50 | Ok(_) => { 51 | toml_str 52 | .parse::() 53 | .map_err(KbdError::TomlError) 54 | .and_then(|v| if let Value::Table(t) = v { 55 | Ok(t) 56 | } else { 57 | Err(KbdError::TomlNotTable) 58 | }) 59 | }, 60 | Err(io_error) => Err(KbdError::IOError(io_error)), 61 | } 62 | }, 63 | Err(io_error) => Err(KbdError::IOError(io_error)), 64 | } 65 | } 66 | 67 | /// Extract a key's value from a table as an int. 68 | pub fn extract_int(table: &mut Table, key: &str) -> KbdResult { 69 | match table.remove(key) { 70 | Some(Value::Integer(i)) => Ok(i), 71 | Some(_) => Err(KbdError::KeyTypeMismatch(key.to_owned(), false)), 72 | None => Err(KbdError::KeyMissing(key.to_owned())), 73 | } 74 | } 75 | 76 | /// Extract a key's value from a table as a string. 77 | pub fn extract_string(table: &mut Table, key: &str) -> KbdResult { 78 | match table.remove(key) { 79 | Some(Value::String(s)) => Ok(s), 80 | Some(_) => Err(KbdError::KeyTypeMismatch(key.to_owned(), false)), 81 | None => Err(KbdError::KeyMissing(key.to_owned())), 82 | } 83 | } 84 | 85 | /// Extract a key's value from a table as a table. 86 | pub fn extract_table(table: &mut Table, key: &str) -> KbdResult
{ 87 | match table.remove(key) { 88 | Some(Value::Table(t)) => Ok(t), 89 | Some(_) => Err(KbdError::KeyTypeMismatch(key.to_owned(), false)), 90 | None => Err(KbdError::KeyMissing(key.to_owned())), 91 | } 92 | } 93 | 94 | /// Extract a key's value from a table as an array. 95 | pub fn extract_array(table: &mut Table, key: &str) -> KbdResult { 96 | match table.remove(key) { 97 | Some(Value::Array(a)) => Ok(a), 98 | Some(_) => Err(KbdError::KeyTypeMismatch(key.to_owned(), false)), 99 | None => Err(KbdError::KeyMissing(key.to_owned())), 100 | } 101 | } 102 | 103 | /// Check for an optional key to extract. 104 | pub fn opt_key(input_result: KbdResult) -> KbdResult> { 105 | match input_result { 106 | Ok(res) => Ok(Some(res)), 107 | Err(KbdError::KeyMissing(_)) => Ok(None), 108 | Err(err) => Err(err), 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /gwm-kbd/src/kbd/desc.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | use std::cmp::Ordering; 36 | use std::process::Command; 37 | use std::str::FromStr; 38 | 39 | use toml::Value; 40 | 41 | use xkb; 42 | 43 | use kbd::err::*; 44 | use kbd::modmask; 45 | 46 | /// An index representing a mode. 47 | pub type Mode = usize; 48 | 49 | /// A mode switching action. 50 | #[derive(Clone, Copy, Debug)] 51 | pub enum ModeSwitchDesc { 52 | /// A mode switching action changing the current mode permanently. 53 | Permanent(Mode), 54 | /// A temporary mode switching action, changing behaviour only for the next chain. 55 | Temporary(Mode), 56 | } 57 | 58 | /// A command to be executed in reaction to specific key events. 59 | #[derive(Debug)] 60 | pub enum CmdDesc { 61 | /// A string to be passed to a shell to execute the command. 62 | Shell(String), 63 | /// A mode to switch to. 64 | ModeSwitch(ModeSwitchDesc), 65 | } 66 | 67 | impl CmdDesc { 68 | /// Run a command and possibly return an resulting mode switching action to perform. 69 | pub fn run(&self) -> Option { 70 | match *self { 71 | CmdDesc::Shell(ref repr) => { 72 | let _ = Command::new("sh").args(&["-c", repr]).spawn(); 73 | None 74 | }, 75 | CmdDesc::ModeSwitch(ref switch) => { 76 | Some(*switch) 77 | }, 78 | } 79 | } 80 | 81 | /// Construct a command from a TOML value. 82 | pub fn from_value(bind_str: String, value: Value) -> KbdResult { 83 | if let Value::String(repr) = value { 84 | Ok(CmdDesc::Shell(repr)) 85 | } else { 86 | Err(KbdError::KeyTypeMismatch(bind_str, true)) 87 | } 88 | } 89 | } 90 | 91 | /// A keysym wrapper used for various trait implementations. 92 | #[derive(PartialEq, Eq, Copy, Clone, Debug)] 93 | pub struct KeysymDesc(xkb::Keysym); 94 | 95 | impl KeysymDesc { 96 | pub fn new(inner: xkb::Keysym) -> Self { 97 | KeysymDesc(inner) 98 | } 99 | } 100 | 101 | impl Ord for KeysymDesc { 102 | fn cmp(&self, other: &KeysymDesc) -> Ordering { 103 | let self_inner: u32 = self.0.into(); 104 | 105 | self_inner.cmp(&other.0.into()) 106 | } 107 | } 108 | 109 | impl PartialOrd for KeysymDesc { 110 | fn partial_cmp(&self, other: &KeysymDesc) -> Option { 111 | Some(self.cmp(other)) 112 | } 113 | } 114 | 115 | impl ::std::fmt::Display for KeysymDesc { 116 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 117 | write!(f, "{}", self.0.utf8()) 118 | } 119 | } 120 | 121 | /// A chord description. 122 | /// 123 | /// A *chord* is a set of modifiers and a key pressed at the same time, represented 124 | /// by a symbolic keysym value (which is independent of keymap). 125 | #[derive(Debug, PartialEq, Eq)] 126 | pub struct ChordDesc { 127 | /// The keysym of the chord. 128 | keysym: KeysymDesc, 129 | /// The modifier mask of the non-depressed mods of the chord. 130 | modmask: xkb::ModMask, 131 | } 132 | 133 | impl Ord for ChordDesc { 134 | fn cmp(&self, other: &ChordDesc) -> Ordering { 135 | let modmask: u32 = self.modmask.into(); 136 | 137 | self.keysym.cmp(&other.keysym).then(modmask.cmp(&other.modmask.into())) 138 | } 139 | } 140 | 141 | impl PartialOrd for ChordDesc { 142 | fn partial_cmp(&self, other: &ChordDesc) -> Option { 143 | Some(self.cmp(other)) 144 | } 145 | } 146 | 147 | impl ChordDesc { 148 | /// Construct a chord description from a string representation of modifiers and a keysym. 149 | /// 150 | /// Assuming no spaces are present in the string, interpret a sequence of `+`-separated 151 | /// modifier descriptions, and a single symbol. Interpolates the `$modkey` variable with the 152 | /// given modifier mask. The part of the string following the first keysym representation is 153 | /// discarded. 154 | pub fn from_string(desc: &str, modkey_mask: xkb::ModMask) -> KbdResult { 155 | let mut modmask = xkb::ModMask(0); 156 | 157 | for word in desc.split('+') { 158 | if word == "$modkey" { 159 | debug!("added default modifier"); 160 | modmask::combine(&mut modmask, modkey_mask); 161 | } else if modmask::from_str(word, &mut modmask) { 162 | debug!("modifier decoded, continuing chord: {} (modmask={:b})", word, modmask.0); 163 | } else if let Ok(sym) = xkb::Keysym::from_str(word) { 164 | debug!("keysym decoded, assuming end of chord: {} ({:?})", word, sym); 165 | modmask::filter_ignore(&mut modmask); 166 | return Ok(ChordDesc { 167 | keysym: KeysymDesc(sym), 168 | modmask, 169 | }); 170 | } else { 171 | error!("could not decode keysym or modifier from word, continuing: {}", word); 172 | } 173 | } 174 | 175 | Err(KbdError::InvalidChord(desc.to_owned())) 176 | } 177 | 178 | pub fn new(keysym: KeysymDesc, mut modmask: xkb::ModMask) -> ChordDesc { 179 | modmask::filter_ignore(&mut modmask); 180 | ChordDesc { keysym, modmask } 181 | } 182 | 183 | pub fn keysym(&self) -> KeysymDesc { 184 | self.keysym 185 | } 186 | 187 | pub fn modmask(&self) -> u16 { 188 | self.modmask.0 as u16 189 | } 190 | } 191 | 192 | /// A chain description. 193 | /// 194 | /// A *chain* is an ordered sequence of chords to be pressed after each other. 195 | #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] 196 | pub struct ChainDesc { 197 | /// The chords in the chain, in order. 198 | chords: Vec, 199 | } 200 | 201 | impl ChainDesc { 202 | /// Construct a chain description from a string representation. 203 | /// 204 | /// Interpret the string as a sequence of space-separated strings representing chords. 205 | pub fn from_string(desc: &str, modkey_mask: xkb::ModMask) -> KbdResult { 206 | let mut chords = Vec::new(); 207 | 208 | for expr in desc.split(' ') { 209 | chords.push(ChordDesc::from_string(expr, modkey_mask)?); 210 | } 211 | 212 | Ok(ChainDesc { chords }) 213 | } 214 | 215 | /// Check if a given chain is a logical prefix of another one. 216 | /// 217 | /// Takes into account ignored modifiers and etc. (NB: not yet correctly implemented). 218 | pub fn is_prefix_of(&self, other: &ChainDesc) -> bool { 219 | other.chords.starts_with(&self.chords) 220 | 221 | // chord comparison mechanism to use: 222 | // (keysym == shortcut_keysym) && 223 | // ((state_mods & ~consumed_mods & significant_mods) == shortcut_mods) 224 | // xkb_state_mod_index_is_active etc 225 | // xkb_state_mod_index_is_consumed etc 226 | } 227 | 228 | pub fn chords(&self) -> &Vec { 229 | &self.chords 230 | } 231 | 232 | pub fn clear(&mut self) { 233 | self.chords.clear(); 234 | } 235 | 236 | pub fn push(&mut self, chord: ChordDesc) { 237 | self.chords.push(chord); 238 | } 239 | 240 | pub fn len(&self) -> usize { 241 | self.chords.len() 242 | } 243 | 244 | pub fn is_empty(&self) -> bool { 245 | self.len() == 0 246 | } 247 | } 248 | 249 | /// A mode description. 250 | #[derive(Debug)] 251 | pub struct ModeDesc { 252 | /// An optional command to execute when the given mode is entered. 253 | enter_cmd: Option, 254 | /// An optional command to execute when the given mode is left. 255 | leave_cmd: Option, 256 | } 257 | 258 | impl ModeDesc { 259 | pub fn new(enter_cmd: Option, leave_cmd: Option) -> ModeDesc { 260 | ModeDesc { enter_cmd, leave_cmd } 261 | } 262 | 263 | pub fn enter_cmd(&self) -> Option<&CmdDesc> { 264 | self.enter_cmd.as_ref() 265 | } 266 | 267 | pub fn leave_cmd(&self) -> Option<&CmdDesc> { 268 | self.leave_cmd.as_ref() 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /gwm-kbd/src/kbd/err.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | use std::io::Error as IoError; 36 | 37 | use getopts; 38 | use toml; 39 | 40 | use xcb::base; 41 | 42 | /// An error occured when interacting with X. 43 | #[derive(Debug)] 44 | pub enum XError { 45 | /// Could not connect to the X server. 46 | CouldNotConnect(base::ConnError), 47 | /// The X server doesn't support XKB. 48 | XKBNotSupported, 49 | /// The call to `use_extension` failed. 50 | UseExtensionError(base::GenericError), 51 | /// No core device could be determined. 52 | CouldNotDetermineCoreDevice, 53 | /// No keymap could be determined. 54 | CouldNotDetermineKeymap, 55 | /// No keyboard state could be determined. 56 | CouldNotDetermineState, 57 | /// The screen being used didn't exist. 58 | CouldNotAcquireScreen, 59 | /// The extension data of the XKB extension could not be determined. 60 | CouldNotGetExtensionData, 61 | /// An I/O error occured. 62 | IOError, 63 | } 64 | 65 | impl XError { 66 | pub fn wrap(self) -> KbdError { 67 | KbdError::X(self) 68 | } 69 | 70 | fn handle(self) -> ! { 71 | use kbd::err::XError::*; 72 | 73 | match self { 74 | CouldNotConnect(e) => error!("Could not connect to X server: {}", e), 75 | XKBNotSupported => error!("The X server doesn't support XKB"), 76 | UseExtensionError(e) => error!("Generic X error: {}", e), 77 | CouldNotDetermineCoreDevice => error!("Could not determine core device ID"), 78 | CouldNotDetermineKeymap => error!("Could not determine core keymap"), 79 | CouldNotDetermineState => error!("Could not determine core state"), 80 | CouldNotAcquireScreen => error!("Screen is invalid"), 81 | CouldNotGetExtensionData => error!("Could not get XKB extension data"), 82 | IOError => error!("An I/O error occured when communicating with the X server"), 83 | } 84 | 85 | ::std::process::exit(1); 86 | } 87 | } 88 | 89 | /// An error occured during operation. 90 | #[derive(Debug)] 91 | pub enum KbdError { 92 | /// Error during command line parsing. 93 | CouldNotParseOptions(getopts::Fail), 94 | /// An I/O error occured. 95 | IOError(IoError), 96 | /// The TOML content of the config file is invalid. 97 | TomlError(toml::de::Error), 98 | /// The TOML file does not contain a toplevel table. 99 | TomlNotTable, 100 | /// A necessary config key is missing. 101 | KeyMissing(String), 102 | /// A config key holds a value of the wrong type. Second field set to true if it's a command 103 | /// key. 104 | KeyTypeMismatch(String, bool), 105 | /// A Keysym could not be parsed. 106 | KeysymCouldNotBeParsed(String), 107 | /// An invalid chord has been passed into the config. 108 | InvalidChord(String), 109 | /// An error encountered when interacting with X. 110 | X(XError), 111 | } 112 | 113 | impl KbdError { 114 | pub fn handle(self) -> ! { 115 | use kbd::err::KbdError::*; 116 | 117 | match self { 118 | CouldNotParseOptions(f) => error!("{}", f), 119 | IOError(i) => error!("I/O error occured: {}", i), 120 | TomlError(t) => error!("TOML parsing of config failed: {}", t), 121 | TomlNotTable => error!("config is not a table at the top level"), 122 | KeyMissing(k) => error!("missing config key: {}", k), 123 | KeyTypeMismatch(k, false) => error!("key {} has incorrect type", k), 124 | KeyTypeMismatch(k, true) => error!("command bound to `{}` has non-string type", k), 125 | KeysymCouldNotBeParsed(k) => error!("could not parse keysym: {}", k), 126 | InvalidChord(d) => error!("chord invalid: {}", d), 127 | X(e) => e.handle(), 128 | } 129 | 130 | ::std::process::exit(1); 131 | } 132 | } 133 | 134 | /// A result returned when reading in the configuration. 135 | pub type KbdResult = Result; 136 | -------------------------------------------------------------------------------- /gwm-kbd/src/kbd/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | pub mod config; 36 | pub mod desc; 37 | pub mod err; 38 | pub mod state; 39 | pub mod modmask; 40 | 41 | pub use self::err::*; 42 | -------------------------------------------------------------------------------- /gwm-kbd/src/kbd/modmask.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | use xcb::xproto; 36 | 37 | use xkb; 38 | 39 | /// Update a given modifier mask. 40 | pub fn combine(mask: &mut xkb::ModMask, add_mask: xkb::ModMask) { 41 | use xcb::ffi::xcb_mod_mask_t; 42 | 43 | *mask = xkb::ModMask(mask.0 as xcb_mod_mask_t | add_mask.0 as xcb_mod_mask_t); 44 | } 45 | 46 | const LOCK_MASK: xkb::ModMask= xkb::ModMask(xproto::MOD_MASK_LOCK); 47 | const NUM_MASK: xkb::ModMask = xkb::ModMask(xproto::MOD_MASK_2); 48 | const IGNORE_MASK: xkb::ModMask = xkb::ModMask(xproto::MOD_MASK_LOCK | xproto::MOD_MASK_2); 49 | 50 | /// Filter ignored modifiers from a mask 51 | pub fn filter_ignore(mask: &mut xkb::ModMask) { 52 | use xcb::ffi::xcb_mod_mask_t; 53 | 54 | *mask = xkb::ModMask(mask.0 as xcb_mod_mask_t & !IGNORE_MASK.0); 55 | } 56 | 57 | /// Construct a set of modifier masks to grab for a keybinding to account for ignored modifiers. 58 | pub fn match_ignore(mask: xkb::ModMask) -> [xkb::ModMask; 4] { 59 | let mut res = [mask, mask, mask, mask]; 60 | 61 | combine(&mut res[1], LOCK_MASK); 62 | combine(&mut res[2], NUM_MASK); 63 | combine(&mut res[3], IGNORE_MASK); 64 | 65 | res 66 | } 67 | 68 | /// Get a modifier mask from a string description of the modifier keys. 69 | pub fn from_str(desc: &str, mask: &mut xkb::ModMask) -> bool { 70 | let mut mod_component: xkb::ModMask = xkb::ModMask(match &desc.to_lowercase()[..] { 71 | "shift" => xproto::MOD_MASK_SHIFT, 72 | "ctrl" => xproto::MOD_MASK_CONTROL, 73 | "mod1" => xproto::MOD_MASK_1, 74 | "mod2" => xproto::MOD_MASK_2, 75 | "mod3" => xproto::MOD_MASK_3, 76 | "mod4" => xproto::MOD_MASK_4, 77 | "mod5" => xproto::MOD_MASK_5, 78 | _ => 0, 79 | }); 80 | 81 | filter_ignore(&mut mod_component); 82 | combine(mask, mod_component); 83 | 84 | mod_component.0 != 0 85 | } 86 | -------------------------------------------------------------------------------- /gwm-kbd/src/kbd/state.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | use std::collections::BTreeMap; 36 | use std::path::Path; 37 | 38 | use toml::value::Value; 39 | 40 | use xcb::base::*; 41 | use xcb::Timestamp; 42 | use xcb::xkb as xxkb; 43 | use xcb::xproto; 44 | 45 | use xkb; 46 | use xkb::{Keycode, Keymap, State}; 47 | use xkb::context::Context; 48 | use xkb::state::{Key, Update}; 49 | 50 | use kbd::config; 51 | use kbd::desc::*; 52 | use kbd::err::*; 53 | use kbd::modmask; 54 | 55 | /// Keyboard state object. 56 | pub struct KbdState<'a> { 57 | /// X connection used to communicate. 58 | con: &'a Connection, 59 | /// Root window. 60 | root: xproto::Window, 61 | /// The XKB library context used. 62 | ctx: Context, 63 | /// The current keymap. 64 | keymap: Keymap, 65 | /// The current keyboard state. 66 | state: State, 67 | /// Dummy keyboard state used to compute keycode and keysym correspondence. 68 | dummy_state: State, 69 | /// Smallest keycode. 70 | min_keycode: Keycode, 71 | /// Largest keycode. 72 | max_keycode: Keycode, 73 | /// Map from keycodes in the index to keysyms the corresponding keys yield. 74 | keysym_map: Vec>, 75 | } 76 | 77 | impl<'a> KbdState<'a> { 78 | /// Construct a new keyboard state object. 79 | pub fn new(con: &'a Connection, screen_num: i32, keymap: Keymap, state: State) 80 | -> KbdResult 81 | { 82 | let setup = con.get_setup(); 83 | let root = if let Some(screen) = setup.roots().nth(screen_num as usize) { 84 | screen.root() 85 | } else { 86 | return Err(XError::CouldNotAcquireScreen.wrap()); 87 | }; 88 | 89 | let dummy_state = keymap.state(); 90 | 91 | let mut state = KbdState { 92 | con, 93 | root, 94 | ctx: Context::default(), 95 | keymap, 96 | state, 97 | dummy_state, 98 | min_keycode: setup.min_keycode().into(), 99 | max_keycode: setup.max_keycode().into(), 100 | keysym_map: Vec::new(), 101 | }; 102 | 103 | state.generate_keysym_map(); 104 | 105 | Ok(state) 106 | } 107 | 108 | /// Update keymap and keyboard state. 109 | fn update_keymap(&mut self) -> KbdResult<()> { 110 | use xkb::x11 as x11; 111 | 112 | let core_dev_id = match x11::device(self.con) { 113 | Ok(id) => id, 114 | Err(()) => return Err(XError::CouldNotDetermineCoreDevice.wrap()), 115 | }; 116 | 117 | self.keymap = match x11::keymap(self.con, core_dev_id, &self.ctx, Default::default()) { 118 | Ok(k) => k, 119 | Err(()) => return Err(XError::CouldNotDetermineKeymap.wrap()), 120 | }; 121 | 122 | self.state = match x11::state(self.con, core_dev_id, &self.keymap) { 123 | Ok(s) => s, 124 | Err(()) => return Err(XError::CouldNotDetermineState.wrap()), 125 | }; 126 | 127 | Ok(()) 128 | } 129 | 130 | fn update_state(&mut self, event: &xxkb::StateNotifyEvent) { 131 | let mut update = Update(&mut self.state); 132 | 133 | update.mask(event.base_mods(), event.latched_mods(), event.locked_mods(), 134 | event.base_group(), event.latched_group(), event.locked_group()); 135 | } 136 | 137 | /// Generate a keysym map from a dummy keyboard state. 138 | fn generate_keysym_map(&mut self) { 139 | fn increment_keycode(keycode: Keycode) -> Keycode { 140 | Keycode(keycode.0 + 1) 141 | } 142 | 143 | let mut keycode = self.min_keycode; 144 | 145 | while keycode != self.max_keycode { 146 | let key = Key(&self.dummy_state, keycode); 147 | let sym = key.sym(); 148 | 149 | debug!("dummy: key {:?} => {:?} ({:?})", 150 | keycode, sym, sym.map_or("".to_owned(), |s| s.utf8())); 151 | 152 | self.keysym_map.push(sym.map(KeysymDesc::new)); 153 | 154 | keycode = increment_keycode(keycode); 155 | } 156 | } 157 | 158 | /// Look up a keycode to determine the keysym produced by it according to the current 159 | /// keyboard state. 160 | fn lookup_keycode(&self, keycode: Keycode) -> Option { 161 | let index = (keycode.0 - self.min_keycode.0) as usize; 162 | 163 | if index <= self.max_keycode.0 as usize { 164 | self.keysym_map[index] 165 | } else { 166 | None 167 | } 168 | } 169 | 170 | /// Look up a keysym to determine the keycode producing it according to the current keyboard 171 | /// state. 172 | fn lookup_keysym(&self, keysym: KeysymDesc) -> Option { 173 | self.keysym_map 174 | .iter() 175 | .position(|e| *e == Some(keysym)) 176 | .map(|pos| Keycode(self.min_keycode.0 + (pos as u32))) 177 | } 178 | 179 | /// Get the connection to the X server. 180 | fn con(&self) -> &Connection { 181 | self.con 182 | } 183 | 184 | /// Get the root window. 185 | fn root(&self) -> xproto::Window { 186 | self.root 187 | } 188 | 189 | fn modmask(&mut self) -> xkb::ModMask { 190 | use xkb::state::component::MODS_EFFECTIVE; 191 | use xkb::state::Serialize; 192 | 193 | Serialize(&mut self.state).mods(MODS_EFFECTIVE) 194 | } 195 | } 196 | 197 | impl<'a> ::std::fmt::Debug for KbdState<'a> { 198 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 199 | write!(f, "(_, {:?}, _, _, _)", self.root) 200 | } 201 | } 202 | 203 | /// Global daemon state object. 204 | #[derive(Debug)] 205 | pub struct DaemonState<'a> { 206 | /// Current keyboard- and other low-level state. 207 | kbd_state: KbdState<'a>, 208 | /// The currently active keymap mode. 209 | current_mode: Mode, 210 | /// The previous mode to switch back to for when the current mode is set temporarily. 211 | previous_mode: Option, 212 | /// The vector of all modes the daemon is aware of. 213 | modes: Vec, 214 | /// The main modkey to use. 215 | modkey_mask: xkb::ModMask, 216 | /// The maximum time between two keypresses in a chain in milliseconds. 217 | keypress_timeout: u32, 218 | /// Currently active chain prefix. 219 | current_chain: ChainDesc, 220 | /// Time at which the last key was pressed. 221 | last_keypress: Timestamp, 222 | /// The bindings registered in all modes. 223 | bindings: BTreeMap<(Mode, ChainDesc), CmdDesc>, 224 | } 225 | 226 | impl<'a> DaemonState<'a> { 227 | /// Construct an initial daemon state from a configuration file. 228 | pub fn from_config(path: &Path, kbd_state: KbdState<'a>) -> KbdResult { 229 | let mut tree = config::parse_file(path)?; 230 | info!("parsed config"); 231 | 232 | let modkey_str = config::extract_string(&mut tree, "modkey")?; 233 | let mut modkey_mask = xkb::ModMask(0); 234 | if modmask::from_str(&modkey_str, &mut modkey_mask) { 235 | info!("determined modkey mask: {} ({:x})", modkey_str, modkey_mask.0); 236 | } else { 237 | error!("could not decode modkey keysym from word, aborting: {}", modkey_str); 238 | return Err(KbdError::KeysymCouldNotBeParsed(modkey_str.to_owned())); 239 | }; 240 | 241 | // set the keypress timeout, defaulting to one second. 242 | let keypress_timeout = 243 | config::opt_key(config::extract_int(&mut tree, "timeout"))?.unwrap_or(1000) as u32; 244 | 245 | let mode_set = config::extract_array(&mut tree, "active_modes")?; 246 | let num_modes = mode_set.len(); 247 | 248 | let mut mode_table = config::extract_table(&mut tree, "modes")?; 249 | let mut i = 0; 250 | 251 | let mut modes = Vec::with_capacity(num_modes); 252 | let mut bindings = BTreeMap::new(); 253 | 254 | for mode_name in mode_set { 255 | let mode_name = if let Value::String(s) = mode_name { 256 | s 257 | } else { 258 | return Err(KbdError::KeyTypeMismatch(format!("active_modes.{}", i), false)); 259 | }; 260 | 261 | let mut mode = config::extract_table(&mut mode_table, &mode_name)?; 262 | 263 | let enter_binding = config::extract_string(&mut mode, "enter_binding")?; 264 | let enter_binding_quick = 265 | config::extract_string(&mut mode, "enter_binding_quick_leave")?; 266 | let enter_cmd = config::opt_key(config::extract_string(&mut mode, "enter_cmd"))? 267 | .map(CmdDesc::Shell); 268 | let leave_cmd = config::opt_key(config::extract_string(&mut mode, "leave_cmd"))? 269 | .map(CmdDesc::Shell); 270 | 271 | debug!("mode: {}", mode_name); 272 | 273 | modes.push(ModeDesc::new(enter_cmd, leave_cmd)); 274 | 275 | let binds = config::extract_table(&mut mode, "bindings")?; 276 | 277 | for (chain_str, cmd_str) in binds { 278 | debug!("=> {} -> {}", chain_str, cmd_str); 279 | bindings 280 | .insert((i, ChainDesc::from_string(&chain_str, modkey_mask)?), 281 | CmdDesc::from_value(chain_str, cmd_str)?); 282 | } 283 | 284 | for j in 0..num_modes { 285 | bindings 286 | .insert((j, ChainDesc::from_string(&enter_binding, modkey_mask)?), 287 | CmdDesc::ModeSwitch(ModeSwitchDesc::Permanent(i))); 288 | bindings 289 | .insert((j, ChainDesc::from_string(&enter_binding_quick, modkey_mask)?), 290 | CmdDesc::ModeSwitch(ModeSwitchDesc::Temporary(i))); 291 | } 292 | 293 | i += 1; 294 | } 295 | 296 | Ok(DaemonState { 297 | kbd_state, 298 | current_mode: 0, 299 | previous_mode: None, 300 | modes, 301 | modkey_mask, 302 | keypress_timeout, 303 | current_chain: ChainDesc::default(), 304 | last_keypress: 0, 305 | bindings, 306 | }) 307 | } 308 | 309 | /// Get the connection to the X server. 310 | fn con(&self) -> &Connection { 311 | self.kbd_state.con() 312 | } 313 | 314 | /// Get the root window. 315 | fn root(&self) -> xproto::Window { 316 | self.kbd_state.root() 317 | } 318 | 319 | /// Grab keys for the current mode. 320 | pub fn grab_current_mode(&self) { 321 | let mut cookies = Vec::new(); 322 | 323 | for &(mode, ref chain) in self.bindings.keys() { 324 | if mode == self.current_mode { 325 | for chord in chain.chords() { 326 | if let Some(keycode) = self.kbd_state.lookup_keysym(chord.keysym()) { 327 | let masks = 328 | modmask::match_ignore(xkb::ModMask(u32::from(chord.modmask()))); 329 | 330 | for mask in &masks { 331 | debug!("grabbing: {:8b}+{} ({})", mask.0, keycode.0, chord.keysym()); 332 | let cookie = 333 | xproto::grab_key(self.con(), true, self.root(), 334 | mask.0 as u16, keycode.0 as u8, 335 | xproto::GRAB_MODE_SYNC as u8, 336 | xproto::GRAB_MODE_ASYNC as u8); 337 | cookies.push(cookie); 338 | } 339 | } 340 | } 341 | } 342 | } 343 | 344 | for cookie in cookies { 345 | if let Err(e) = cookie.request_check() { 346 | error!("encountered error grabbing keys: {}", e); 347 | } 348 | } 349 | } 350 | 351 | /// Ungrab all keys from the current mode. 352 | /// 353 | /// Ungrabs all keys for simplicity. 354 | fn ungrab_current_mode(&self) { 355 | let err = xproto::ungrab_key(self.con(), 356 | xproto::GRAB_ANY as u8, 357 | self.root(), 358 | xproto::MOD_MASK_ANY as u16) 359 | .request_check() 360 | .is_err(); 361 | 362 | if err { 363 | error!("could not ungrab keys"); 364 | } 365 | } 366 | 367 | /// Fall back to a mode possibly stored in the `previous_mode` field. 368 | fn fallback_mode(&mut self) { 369 | if let Some(fallback_mode) = self.previous_mode { 370 | info!("falling back to mode"); 371 | self.switch_mode(ModeSwitchDesc::Permanent(fallback_mode)); 372 | } 373 | } 374 | 375 | /// Switch modes according to directive. 376 | /// 377 | /// Manages internal state, as well as necessary interaction with the X server. 378 | fn switch_mode(&mut self, switch: ModeSwitchDesc) { 379 | let new_mode = match switch { 380 | ModeSwitchDesc::Permanent(new_mode) => { 381 | self.previous_mode = None; 382 | new_mode 383 | }, 384 | ModeSwitchDesc::Temporary(new_mode) => { 385 | self.previous_mode = Some(self.current_mode); 386 | new_mode 387 | }, 388 | }; 389 | 390 | if new_mode == self.current_mode { 391 | self.previous_mode = None; 392 | return; 393 | } 394 | 395 | if let Some(cmd) = self.modes[self.current_mode].leave_cmd() { 396 | cmd.run(); 397 | } 398 | 399 | self.current_mode = new_mode; 400 | 401 | if let Some(cmd) = self.modes[self.current_mode].enter_cmd() { 402 | cmd.run(); 403 | } 404 | 405 | self.ungrab_current_mode(); 406 | self.grab_current_mode(); 407 | } 408 | 409 | /// Process a chord determined from a key press event. 410 | /// 411 | /// Dispatches to command execution and mode switching logic according to configuration. 412 | fn process_chord(&mut self, keycode: Keycode, time: xproto::Timestamp) { 413 | let keysym = if let Some(sym) = self.kbd_state.lookup_keycode(keycode) { 414 | debug!("key pressed:: keycode={:?} (sym={})", keycode, sym); 415 | 416 | sym 417 | } else { 418 | // we don't actually expect this to happen, at least in X11, because we don't grab 419 | // keys we don't need. 420 | debug!("key pressed: keycode={:?} (no sym)", keycode); 421 | 422 | self.fallback_mode(); 423 | return; 424 | }; 425 | 426 | let chord = ChordDesc::new(keysym, self.kbd_state.modmask()); 427 | let mut drop_chain = true; 428 | let mut mode_switch = None; 429 | 430 | if self.last_keypress + self.keypress_timeout < time { 431 | self.current_chain.clear(); 432 | } 433 | 434 | self.current_chain.push(chord); 435 | 436 | for (&(_, ref chain), cmd) in 437 | self.bindings.iter().filter(|k| (k.0).0 == self.current_mode) { 438 | if self.current_chain.is_prefix_of(chain) { 439 | if self.current_chain.len() == chain.len() { 440 | info!("determined command {:?} from chain {:?}", cmd, self.current_chain); 441 | mode_switch = cmd.run(); 442 | 443 | drop_chain = true; 444 | break; 445 | } 446 | 447 | drop_chain = false; 448 | } 449 | } 450 | 451 | if drop_chain { 452 | self.current_chain.clear(); 453 | } 454 | 455 | if let Some(switch) = mode_switch { 456 | self.switch_mode(switch); 457 | } else { 458 | self.fallback_mode(); 459 | } 460 | 461 | self.last_keypress = time; 462 | } 463 | 464 | /// Run the main loop of the daemon. 465 | pub fn run(&mut self) -> KbdResult<()> { 466 | let xkb_base = if let Some(data) = self.con().get_extension_data(&mut xxkb::id()) { 467 | data.first_event() 468 | } else { 469 | return Err(XError::CouldNotGetExtensionData.wrap()); 470 | }; 471 | 472 | debug!("xkb base: {}", xkb_base); 473 | 474 | loop { 475 | self.con().flush(); 476 | let event = if let Some(e) = self.con().wait_for_event() { 477 | e 478 | } else { 479 | return Err(XError::IOError.wrap()); 480 | }; 481 | 482 | if event.response_type() == xkb_base { 483 | let xkb_type = { 484 | let event = unsafe { cast_event::(&event) }; 485 | event.xkb_type() 486 | }; 487 | 488 | match xkb_type { 489 | xxkb::NEW_KEYBOARD_NOTIFY => { 490 | debug!("xkb event: NEW_KEYBOARD_NOTIFY"); 491 | let event = unsafe { 492 | cast_event::(&event) 493 | }; 494 | 495 | if event.changed() & xxkb::NKN_DETAIL_KEYCODES as u16 != 0 { 496 | info!("updated keymap (new keyboard)"); 497 | if let Err(e) = self.kbd_state.update_keymap() { 498 | e.handle(); 499 | } 500 | } 501 | }, 502 | xxkb::MAP_NOTIFY => { 503 | debug!("xkb event: MAP_NOTIFY"); 504 | 505 | if let Err(e) = self.kbd_state.update_keymap() { 506 | e.handle(); 507 | } 508 | }, 509 | xxkb::STATE_NOTIFY => { 510 | let event = unsafe { cast_event::(&event) }; 511 | debug!("xkb event: STATE_NOTIFY mods={:?}", event.mods()); 512 | 513 | self.kbd_state.update_state(event); 514 | }, 515 | t => { 516 | debug!("xkb event (unknown): {}", t); 517 | }, 518 | } 519 | } else { 520 | match event.response_type() { 521 | xproto::KEY_PRESS => { 522 | debug!("generic event: KEY_PRESS"); 523 | let event = unsafe { cast_event::(&event) }; 524 | let keycode = Keycode(u32::from(event.detail())); 525 | 526 | self.process_chord(keycode, event.time()); 527 | }, 528 | xproto::KEY_RELEASE => { 529 | debug!("generic event: KEY_RELEASE"); 530 | }, 531 | t => { 532 | debug!("generic event (unknown): {}", t); 533 | }, 534 | } 535 | } 536 | } 537 | } 538 | } 539 | -------------------------------------------------------------------------------- /gwm-kbd/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | extern crate getopts; 36 | #[macro_use] 37 | extern crate log; 38 | extern crate toml; 39 | extern crate xcb; 40 | extern crate xkb; 41 | 42 | pub mod kbd; 43 | -------------------------------------------------------------------------------- /gwm-kbd/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Inokentiy Babushkin and contributors (c) 2016-2017 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials provided 16 | * with the distribution. 17 | * 18 | * * Neither the name of Inokentiy Babushkin nor the names of other 19 | * contributors may be used to endorse or promote products derived 20 | * from this software without specific prior written permission. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | extern crate env_logger; 36 | extern crate getopts; 37 | extern crate gwm_kbd; 38 | #[macro_use] 39 | extern crate log; 40 | extern crate xcb; 41 | extern crate xkb; 42 | 43 | use getopts::Options; 44 | 45 | use std::env; 46 | use std::mem; 47 | use std::path::{Path, PathBuf}; 48 | 49 | use xcb::base::*; 50 | use xcb::ffi::xkb as xxkb_ffi; 51 | use xcb::xkb as xxkb; 52 | 53 | use xkb::context::Context; 54 | use xkb::x11 as x11; 55 | 56 | use gwm_kbd::kbd::err::{KbdError, KbdResult, XError}; 57 | use gwm_kbd::kbd::state::{DaemonState, KbdState}; 58 | 59 | /// Initialize the logger. 60 | fn setup_logger() { 61 | env_logger::init(); 62 | info!("initialized logger"); 63 | 64 | // clean environment for cargo and other programs honoring `RUST_LOG` 65 | env::remove_var("RUST_LOG"); 66 | } 67 | 68 | /// Main routine. 69 | fn do_main(path: &Path) -> KbdResult<()> { 70 | let (con, screen_num) = match Connection::connect(None) { 71 | Ok(c) => c, 72 | Err(e) => { 73 | return Err(XError::CouldNotConnect(e).wrap()); 74 | }, 75 | }; 76 | 77 | let cookie = 78 | xxkb::use_extension(&con, x11::MIN_MAJOR_XKB_VERSION, x11::MIN_MINOR_XKB_VERSION); 79 | match cookie.get_reply() { 80 | Ok(r) => { 81 | if !r.supported() { 82 | return Err(XError::XKBNotSupported.wrap()); 83 | } 84 | }, 85 | Err(e) => { 86 | return Err(XError::UseExtensionError(e).wrap()); 87 | }, 88 | }; 89 | 90 | let core_dev_id = match x11::device(&con) { 91 | Ok(id) => id, 92 | Err(()) => return Err(XError::CouldNotDetermineCoreDevice.wrap()), 93 | }; 94 | let context = Context::default(); 95 | let keymap = match x11::keymap(&con, core_dev_id, &context, Default::default()) { 96 | Ok(k) => k, 97 | Err(()) => return Err(XError::CouldNotDetermineKeymap.wrap()), 98 | }; 99 | 100 | let state = match x11::state(&con, core_dev_id, &keymap) { 101 | Ok(s) => s, 102 | Err(()) => return Err(XError::CouldNotDetermineState.wrap()), 103 | }; 104 | 105 | let events = 106 | (xxkb::EVENT_TYPE_NEW_KEYBOARD_NOTIFY | 107 | xxkb::EVENT_TYPE_MAP_NOTIFY | 108 | xxkb::EVENT_TYPE_STATE_NOTIFY) as u16; 109 | 110 | let nkn_details = xxkb::NKN_DETAIL_KEYCODES as u16; 111 | 112 | let map_parts = 113 | (xxkb::MAP_PART_KEY_TYPES | 114 | xxkb::MAP_PART_KEY_SYMS | 115 | xxkb::MAP_PART_MODIFIER_MAP | 116 | xxkb::MAP_PART_EXPLICIT_COMPONENTS | 117 | xxkb::MAP_PART_KEY_ACTIONS | 118 | // xxkb::MAP_PART_KEY_BEHAVIORS | 119 | xxkb::MAP_PART_VIRTUAL_MODS | 120 | xxkb::MAP_PART_VIRTUAL_MOD_MAP) as u16; 121 | 122 | let state_details = 123 | (xxkb::STATE_PART_MODIFIER_BASE | 124 | xxkb::STATE_PART_MODIFIER_LATCH | 125 | xxkb::STATE_PART_MODIFIER_LOCK | 126 | xxkb::STATE_PART_GROUP_BASE | 127 | xxkb::STATE_PART_GROUP_LATCH | 128 | xxkb::STATE_PART_GROUP_LOCK) as u16; 129 | 130 | let mut details: xxkb_ffi::xcb_xkb_select_events_details_t = unsafe { mem::zeroed() }; 131 | details.affectNewKeyboard = nkn_details; 132 | details.newKeyboardDetails = nkn_details; 133 | details.affectState = state_details; 134 | details.stateDetails = state_details; 135 | 136 | let cookie = unsafe { 137 | let c = xxkb_ffi::xcb_xkb_select_events_checked( 138 | con.get_raw_conn(), 139 | xxkb::ID_USE_CORE_KBD as xxkb_ffi::xcb_xkb_device_spec_t, /* device_spec */ 140 | events as u16, /* affect_which */ 141 | 0, /* clear */ 0, /* select_all */ 142 | map_parts as u16, /* affect_map */ 143 | map_parts as u16, /* map */ 144 | &details as *const xxkb_ffi::xcb_xkb_select_events_details_t); 145 | 146 | VoidCookie { 147 | cookie: c, 148 | conn: &con, 149 | checked: true, 150 | } 151 | }; 152 | 153 | // TODO: proper error handling 154 | cookie.request_check().expect("no events selected"); 155 | 156 | let flags = 157 | xxkb::PER_CLIENT_FLAG_GRABS_USE_XKB_STATE | 158 | xxkb::PER_CLIENT_FLAG_LOOKUP_STATE_WHEN_GRABBED; 159 | 160 | let cookie = 161 | xxkb::per_client_flags(&con, xxkb::ID_USE_CORE_KBD as u16, flags, flags, 0, 0, 0); 162 | 163 | // TODO: proper error handling 164 | cookie.get_reply().expect("no flags set"); 165 | 166 | let kbd_state = KbdState::new(&con, screen_num, keymap, state)?; 167 | let mut daemon_state = 168 | DaemonState::from_config(path, kbd_state)?; 169 | debug!("initial daemon state: {:?}", daemon_state); 170 | 171 | daemon_state.grab_current_mode(); 172 | daemon_state.run() 173 | } 174 | 175 | fn main() { 176 | let args: Vec = env::args().collect(); 177 | 178 | // set up option parsing 179 | let mut opts = Options::new(); 180 | opts.optopt("c", "config", "set config file name", "FILE"); 181 | opts.optflag("h", "help", "print this help menu"); 182 | 183 | // match on args and decide what to do 184 | let matches = match opts.parse(&args[1..]) { 185 | Ok(m) => m, 186 | Err(f) => KbdError::CouldNotParseOptions(f).handle(), 187 | }; 188 | 189 | if matches.opt_present("h") { 190 | let brief = format!("Usage: {} [options]", &args[0]); 191 | eprintln!("{}", opts.usage(&brief)); 192 | return; 193 | } 194 | 195 | let config_path = if let Some(p) = matches.opt_str("c") { 196 | p.into() 197 | } else if let Some(mut buf) = env::home_dir() { 198 | buf.push(".gwmkbdrc"); 199 | buf 200 | } else { 201 | warn!("couldn't determine the value of $HOME, using current dir"); 202 | PathBuf::from("gwmkbdrc") 203 | }; 204 | 205 | setup_logger(); 206 | 207 | match do_main(&config_path) { 208 | Ok(()) => ::std::process::exit(0), 209 | Err(e) => e.handle(), 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibabushkin/gabelstaplerwm/b7950e2443f871cf270c35b8671ce3627790c26c/screenshot.png -------------------------------------------------------------------------------- /xephyr: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | Xephyr -ac -br -noreset -resizeable -screen 1366x768 :1.0 & 3 | sleep 1 4 | # TODO: run with proper path 5 | if [ "$1" = "-k" ]; then 6 | RUST_BACKTRACE=1 RUST_LOG=debug DISPLAY=:1.0 "./target/debug/gwm-kbd" 7 | else 8 | RUST_BACKTRACE=1 RUST_LOG=debug DISPLAY=:1.0 "./target/debug/gwm-core" 9 | fi 10 | -------------------------------------------------------------------------------- /xinitrc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DISPLAY=:1.0 RUST_BACKTRACE=1 RUST_LOG=debug exec ./target/debug/gwm-kbd 2> wm_log > tags_log 3 | --------------------------------------------------------------------------------