├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── commands.rs ├── config.rs ├── core.rs ├── ewmh.rs ├── keycode.rs ├── layout.rs ├── stack.rs ├── state.rs ├── statusbar.rs ├── utils.rs ├── workspace.rs ├── xlib_window_system.rs └── xr3wm.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | os: [ubuntu-latest] 10 | rust: [stable, nightly] 11 | steps: 12 | - uses: hecrj/setup-rust-action@v1 13 | with: 14 | rust-version: ${{ matrix.rust }} 15 | components: 'rustfmt, clippy' 16 | - uses: actions/checkout@master 17 | - name: Install dependencies 18 | run: sudo apt install -y libxinerama1 libxinerama-dev 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --no-fail-fast --verbose 23 | - name: Run clippy 24 | run: cargo clippy --all-targets --all-features -- -D warnings 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | *.swp 7 | tags 8 | todo 9 | .issues/ 10 | 11 | # Executables 12 | *.exe 13 | 14 | # Generated by Cargo 15 | /target/ 16 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.80" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" 25 | dependencies = [ 26 | "backtrace", 27 | ] 28 | 29 | [[package]] 30 | name = "backtrace" 31 | version = "0.3.69" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 34 | dependencies = [ 35 | "addr2line", 36 | "cc", 37 | "cfg-if", 38 | "libc", 39 | "miniz_oxide", 40 | "object", 41 | "rustc-demangle", 42 | ] 43 | 44 | [[package]] 45 | name = "cc" 46 | version = "1.0.88" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" 49 | 50 | [[package]] 51 | name = "cfg-if" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 55 | 56 | [[package]] 57 | name = "env_filter" 58 | version = "0.1.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 61 | dependencies = [ 62 | "log", 63 | ] 64 | 65 | [[package]] 66 | name = "env_logger" 67 | version = "0.11.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" 70 | dependencies = [ 71 | "env_filter", 72 | "log", 73 | ] 74 | 75 | [[package]] 76 | name = "erased-serde" 77 | version = "0.4.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "388979d208a049ffdfb22fa33b9c81942215b940910bccfe258caeb25d125cb3" 80 | dependencies = [ 81 | "serde", 82 | ] 83 | 84 | [[package]] 85 | name = "gimli" 86 | version = "0.28.1" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 89 | 90 | [[package]] 91 | name = "inventory" 92 | version = "0.3.15" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" 95 | 96 | [[package]] 97 | name = "itoa" 98 | version = "1.0.10" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 101 | 102 | [[package]] 103 | name = "libc" 104 | version = "0.2.153" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 107 | 108 | [[package]] 109 | name = "libloading" 110 | version = "0.8.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164" 113 | dependencies = [ 114 | "cfg-if", 115 | "windows-targets", 116 | ] 117 | 118 | [[package]] 119 | name = "log" 120 | version = "0.4.21" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 123 | 124 | [[package]] 125 | name = "memchr" 126 | version = "2.7.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 129 | 130 | [[package]] 131 | name = "miniz_oxide" 132 | version = "0.7.2" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 135 | dependencies = [ 136 | "adler", 137 | ] 138 | 139 | [[package]] 140 | name = "object" 141 | version = "0.32.2" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 144 | dependencies = [ 145 | "memchr", 146 | ] 147 | 148 | [[package]] 149 | name = "once_cell" 150 | version = "1.19.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 153 | 154 | [[package]] 155 | name = "pkg-config" 156 | version = "0.3.30" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 159 | 160 | [[package]] 161 | name = "proc-macro2" 162 | version = "1.0.78" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 165 | dependencies = [ 166 | "unicode-ident", 167 | ] 168 | 169 | [[package]] 170 | name = "quote" 171 | version = "1.0.35" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 174 | dependencies = [ 175 | "proc-macro2", 176 | ] 177 | 178 | [[package]] 179 | name = "rustc-demangle" 180 | version = "0.1.23" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 183 | 184 | [[package]] 185 | name = "ryu" 186 | version = "1.0.17" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 189 | 190 | [[package]] 191 | name = "serde" 192 | version = "1.0.197" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 195 | dependencies = [ 196 | "serde_derive", 197 | ] 198 | 199 | [[package]] 200 | name = "serde_derive" 201 | version = "1.0.197" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 204 | dependencies = [ 205 | "proc-macro2", 206 | "quote", 207 | "syn", 208 | ] 209 | 210 | [[package]] 211 | name = "serde_json" 212 | version = "1.0.114" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 215 | dependencies = [ 216 | "itoa", 217 | "ryu", 218 | "serde", 219 | ] 220 | 221 | [[package]] 222 | name = "syn" 223 | version = "2.0.52" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" 226 | dependencies = [ 227 | "proc-macro2", 228 | "quote", 229 | "unicode-ident", 230 | ] 231 | 232 | [[package]] 233 | name = "typetag" 234 | version = "0.2.16" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "661d18414ec032a49ece2d56eee03636e43c4e8d577047ab334c0ba892e29aaf" 237 | dependencies = [ 238 | "erased-serde", 239 | "inventory", 240 | "once_cell", 241 | "serde", 242 | "typetag-impl", 243 | ] 244 | 245 | [[package]] 246 | name = "typetag-impl" 247 | version = "0.2.16" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "ac73887f47b9312552aa90ef477927ff014d63d1920ca8037c6c1951eab64bb1" 250 | dependencies = [ 251 | "proc-macro2", 252 | "quote", 253 | "syn", 254 | ] 255 | 256 | [[package]] 257 | name = "unicode-ident" 258 | version = "1.0.12" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 261 | 262 | [[package]] 263 | name = "windows-targets" 264 | version = "0.52.4" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 267 | dependencies = [ 268 | "windows_aarch64_gnullvm", 269 | "windows_aarch64_msvc", 270 | "windows_i686_gnu", 271 | "windows_i686_msvc", 272 | "windows_x86_64_gnu", 273 | "windows_x86_64_gnullvm", 274 | "windows_x86_64_msvc", 275 | ] 276 | 277 | [[package]] 278 | name = "windows_aarch64_gnullvm" 279 | version = "0.52.4" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 282 | 283 | [[package]] 284 | name = "windows_aarch64_msvc" 285 | version = "0.52.4" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 288 | 289 | [[package]] 290 | name = "windows_i686_gnu" 291 | version = "0.52.4" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 294 | 295 | [[package]] 296 | name = "windows_i686_msvc" 297 | version = "0.52.4" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 300 | 301 | [[package]] 302 | name = "windows_x86_64_gnu" 303 | version = "0.52.4" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 306 | 307 | [[package]] 308 | name = "windows_x86_64_gnullvm" 309 | version = "0.52.4" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 312 | 313 | [[package]] 314 | name = "windows_x86_64_msvc" 315 | version = "0.52.4" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 318 | 319 | [[package]] 320 | name = "x11" 321 | version = "2.21.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" 324 | dependencies = [ 325 | "libc", 326 | "pkg-config", 327 | ] 328 | 329 | [[package]] 330 | name = "xr3wm" 331 | version = "0.0.1" 332 | dependencies = [ 333 | "anyhow", 334 | "env_logger", 335 | "libc", 336 | "libloading", 337 | "log", 338 | "serde", 339 | "serde_json", 340 | "typetag", 341 | "x11", 342 | ] 343 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xr3wm" 3 | version = "0.0.1" 4 | authors = ["Cristian Kubis "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | log = "0.4" 9 | env_logger = { version = "0.11", default-features = false } 10 | anyhow = { version = "1.0", features = ["backtrace"] } 11 | libloading = "0.8" 12 | typetag = { version = "0.2", optional = true } 13 | serde = { version = "1.0", optional = true } 14 | serde_json = { version = "1.0", optional = true } 15 | libc = "0.2" 16 | x11 = { version = "2.21", features = ["xlib", "xinerama"] } 17 | 18 | [features] 19 | default = ["reload"] 20 | reload = ["dep:typetag", "dep:serde", "dep:serde_json"] 21 | 22 | [lib] 23 | name = "xr3wm" 24 | path = "src/core.rs" 25 | 26 | [[bin]] 27 | name = "xr3wm" 28 | path = "src/xr3wm.rs" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Cristian Kubis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xr3wm ![](https://github.com/tsurai/xr3wm/workflows/build/badge.svg) 2 | 3 | i3 and xmonad inspiered tiling window manager written in Rust. 4 | 5 | ## ToDo 6 | 7 | - [ ] improve key mappings 8 | - [ ] improve config usability 9 | - [ ] user documentation 10 | 11 | ## Project status 12 | xr3wm contains fairly old and dirty code which has grown out of control over time. It will be completly reworked in the future 13 | 14 | ## Credits 15 | 16 | Please have a look at [Kintaros](https://github.com/Kintaro) wm [wtftw](https://github.com/Kintaro/wtftw). He helped me a lot with my implementation and stupid questions. 17 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | extern crate libc; 4 | 5 | use crate::config::Config; 6 | use crate::layout::{Layout, LayoutMsg}; 7 | use crate::xlib_window_system::XlibWindowSystem; 8 | use crate::state::WmState; 9 | use crate::workspace::MoveOp; 10 | use crate::utils::exec; 11 | use std::process::Child; 12 | use x11::xlib::Window; 13 | use anyhow::{Context, Result}; 14 | 15 | type CustomCmdFn = dyn Fn(&WmState) -> Result, String>; 16 | 17 | pub enum Cmd { 18 | Custom(Box), 19 | Exec(String, Vec), 20 | SpawnTerminal(Vec), 21 | SwitchWorkspace(usize), 22 | SwitchScreen(usize), 23 | MoveToWorkspace(usize), 24 | MoveToScreen(usize), 25 | SendLayoutMsg(LayoutMsg), 26 | NestLayout(Box Box>), 27 | Reload(Vec<(String,String)>), 28 | RemoveNested, 29 | Exit, 30 | KillClient, 31 | FocusParentUp, 32 | FocusParentDown, 33 | FocusParentMaster, 34 | FocusUp, 35 | FocusDown, 36 | FocusMaster, 37 | SwapUp, 38 | SwapDown, 39 | SwapMaster, 40 | SwapParentUp, 41 | SwapParentDown, 42 | SwapParentMaster, 43 | } 44 | 45 | impl Cmd { 46 | pub fn call(&self, xws: &mut XlibWindowSystem, state: &mut WmState, config: &Config, bar_handle: Option<&mut Child>) -> Result<()> { 47 | match self { 48 | Cmd::Custom(func) => { 49 | debug!("Cmd::Custom"); 50 | match func(state) { 51 | Ok(Some(cmd)) => cmd.call(xws, state, config, bar_handle)?, 52 | Ok(None) => (), 53 | Err(e) => error!("Cmd::Custom failed: {}", e), 54 | } 55 | } 56 | Cmd::Exec(ref cmd, ref args) => { 57 | debug!("Cmd::Exec: {} {:?}", cmd, args); 58 | exec(cmd.clone(), args.clone()); 59 | } 60 | Cmd::SpawnTerminal(ref args) => { 61 | debug!("Cmd::SpawnTerminal: {} {:?}", config.terminal, args); 62 | exec(config.terminal.clone(), args.clone()); 63 | } 64 | Cmd::SwitchWorkspace(index) => { 65 | debug!("Cmd::SwitchWorkspace: {}", index); 66 | state.switch_to_ws(xws, config, index - 1, true); 67 | } 68 | Cmd::SwitchScreen(screen) => { 69 | debug!("Cmd::SwitchScreen: {}", screen); 70 | state.switch_to_screen(xws, config, screen - 1); 71 | } 72 | Cmd::MoveToWorkspace(index) => { 73 | debug!("Cmd::MoveToWorkspace: {}", index); 74 | state.move_window_to_ws(xws, config, index - 1); 75 | } 76 | Cmd::MoveToScreen(screen) => { 77 | debug!("Cmd::MoveToScreen: {}", screen); 78 | state.move_window_to_screen(xws, config, screen - 1); 79 | } 80 | Cmd::SendLayoutMsg(ref msg) => { 81 | debug!("Cmd::SendLayoutMsg::{:?}", msg); 82 | state.current_ws_mut().send_layout_message(xws, msg.clone()); 83 | state.redraw_current(xws, config); 84 | } 85 | Cmd::NestLayout(layout_fn) => { 86 | let layout = layout_fn(); 87 | debug!("Cmd::NestLayout: {}", layout.name()); 88 | state.current_ws_mut().nest_layout(layout); 89 | } 90 | Cmd::RemoveNested => { 91 | debug!("Cmd::RemoveNested"); 92 | state.current_ws_mut().managed.dissolve_container(); 93 | state.redraw_current(xws, config); 94 | } 95 | Cmd::Reload(envs) => { 96 | debug!("Cmd::Reload"); 97 | 98 | #[cfg(feature = "reload")] 99 | reload(config, state, bar_handle, envs.as_slice()) 100 | .context("failed to reload xr3wm")?; 101 | 102 | #[cfg(not(feature = "reload"))] 103 | warn!("missing reload support. Recompile with the reload feature enabled"); 104 | } 105 | Cmd::Exit => { 106 | debug!("Cmd::Exit"); 107 | xws.close(); 108 | } 109 | Cmd::KillClient => { 110 | if let Some(window) = state.current_ws().focused_window() { 111 | debug!("Cmd::KillClient: {:#x}", window); 112 | xws.kill_window(window); 113 | } 114 | } 115 | Cmd::FocusUp | Cmd::FocusDown | Cmd::FocusMaster | Cmd::FocusParentUp | Cmd::FocusParentDown | Cmd::FocusParentMaster => { 116 | if let Some(window) = state.current_ws().focused_window() { 117 | let workspace = state.current_ws_mut(); 118 | let new_focus = match self { 119 | Cmd::FocusUp => { 120 | debug!("Cmd::FocusUp: {:#x}", window); 121 | workspace.move_focus(MoveOp::Up) 122 | } 123 | Cmd::FocusDown => { 124 | debug!("Cmd::FocusDown: {:#x}", window); 125 | workspace.move_focus(MoveOp::Down) 126 | } 127 | Cmd::FocusMaster => { 128 | debug!("Cmd::FocusMaster: {:#x}", window); 129 | workspace.move_focus(MoveOp::Swap) 130 | } 131 | Cmd::FocusParentUp => { 132 | debug!("Cmd::FocusParentUp: {:#x}", window); 133 | workspace.move_parent_focus(MoveOp::Up) 134 | } 135 | Cmd::FocusParentDown => { 136 | debug!("Cmd::FocusParentDown: {:#x}", window); 137 | workspace.move_parent_focus(MoveOp::Down) 138 | } 139 | Cmd::FocusParentMaster => { 140 | debug!("Cmd::FocusParentMaster: {:#x}", window); 141 | workspace.move_parent_focus(MoveOp::Swap) 142 | } 143 | _ => None 144 | }; 145 | 146 | if let Some(window) = new_focus { 147 | xws.focus_window(window); 148 | } 149 | } 150 | }, 151 | Cmd::SwapUp | Cmd::SwapDown | Cmd::SwapMaster | Cmd::SwapParentUp | Cmd::SwapParentDown | Cmd::SwapParentMaster => { 152 | if let Some(window) = state.current_ws().focused_window() { 153 | let workspace = state.current_ws_mut(); 154 | let new_focus = match self { 155 | Cmd::SwapUp => { 156 | debug!("Cmd::SwapUp: {:#x}", window); 157 | workspace.move_window(MoveOp::Up) 158 | } 159 | Cmd::SwapDown => { 160 | debug!("Cmd::SwapDown: {:#x}", window); 161 | workspace.move_window(MoveOp::Down) 162 | } 163 | Cmd::SwapMaster => { 164 | debug!("Cmd::SwapMaster: {:#x}", window); 165 | workspace.move_window(MoveOp::Swap) 166 | } 167 | Cmd::SwapParentUp => { 168 | debug!("Cmd::SwapParentUp: {:#x}", window); 169 | workspace.move_parent_window(MoveOp::Up) 170 | } 171 | Cmd::SwapParentDown => { 172 | debug!("Cmd::SwapParentDown: {:#x}", window); 173 | workspace.move_parent_window(MoveOp::Down) 174 | } 175 | Cmd::SwapParentMaster => { 176 | debug!("Cmd::SwapParentMaster: {:#x}", window); 177 | workspace.move_parent_window(MoveOp::Swap) 178 | } 179 | _ => false 180 | }; 181 | 182 | if new_focus { 183 | state.redraw_current(xws, config); 184 | xws.skip_enter_events(); 185 | } 186 | } 187 | } 188 | } 189 | Ok(()) 190 | } 191 | } 192 | 193 | #[cfg(feature = "reload")] 194 | fn reload(config: &Config, state: &WmState, mut bar_handle: Option<&mut Child>, custom_envs: &[(String, String)]) -> Result<()> { 195 | use std::{env, iter}; 196 | use self::libc::execvpe; 197 | use std::path::Path; 198 | use std::ptr::null; 199 | use std::ffi::CString; 200 | use std::io::Write; 201 | use std::fs::{self, File, OpenOptions}; 202 | use anyhow::{anyhow, bail}; 203 | 204 | info!("recompiling..."); 205 | 206 | if let Some(ref mut handle) = bar_handle { 207 | let std = handle.stdin 208 | .as_mut() 209 | .ok_or_else(|| anyhow!("failed to get statusbar stdin"))?; 210 | 211 | std.write_all(b"recompiling xr3wm...\n").ok(); 212 | std.flush().ok(); 213 | } 214 | 215 | let log_path = Path::new("/tmp/xr3wm_build_err.log"); 216 | 217 | if let Err(err_msg) = Config::compile() { 218 | let mut file = File::create(log_path)?; 219 | file.write_all(format!("{err_msg}").as_bytes())?; 220 | file.sync_all()?; 221 | 222 | exec(config.terminal.clone(), vec!["-e".into(), format!("less {}", log_path.to_str().unwrap_or(""))]); 223 | 224 | bail!(err_msg) 225 | } else if log_path.try_exists().unwrap_or(false) { 226 | fs::remove_file("/tmp/xr3wm_build_err.log").ok(); 227 | } 228 | 229 | debug!("Cmd::Reload: restarting xr3wm..."); 230 | 231 | let path = Path::new(&Config::get_dir()?).join(".state.tmp"); 232 | 233 | // save current workspace states to restore on restart 234 | let file = OpenOptions::new() 235 | .create(true) 236 | .write(true) 237 | .truncate(true) 238 | .open(path) 239 | .context("failed to open workspace state tmp file")?; 240 | 241 | serde_json::to_writer(file, state) 242 | .context("failed to serialize workspace states")?; 243 | 244 | let args: Vec<*const libc::c_char> = env::args() 245 | .filter_map(|x| CString::new(x).ok()) 246 | .map(|x| x.into_raw() as *const libc::c_char) 247 | .chain(iter::once(null())) 248 | .collect(); 249 | 250 | let envs: Vec<*const libc::c_char> = custom_envs 251 | .iter() 252 | .map(|(k,v)| format!("{k}={v}")) 253 | .chain(env::vars().map(|(k,v)| format!("{k}={v}"))) 254 | .filter_map(|x| CString::new(x).ok()) 255 | .map(|x| x.into_raw() as *const libc::c_char) 256 | .chain(iter::once(null())) 257 | .collect(); 258 | 259 | // kill statusbar to avoid leaving a zombie 260 | if let Some(handle) = bar_handle { 261 | handle.kill().ok(); 262 | handle.wait().ok(); 263 | } 264 | 265 | unsafe { 266 | execvpe(args[0] as *const libc::c_char, args.as_ptr(), envs.as_ptr()); 267 | // execvp returns only if an error has occurred 268 | error!("failed to reload: {}", ::std::io::Error::last_os_error()); 269 | } 270 | 271 | Ok(()) 272 | } 273 | 274 | pub struct ManageHook { 275 | pub class_name: String, 276 | pub cmd: CmdManage, 277 | } 278 | 279 | pub enum CmdManage { 280 | Move(usize), 281 | Float, 282 | Fullscreen, 283 | Ignore, 284 | } 285 | 286 | impl CmdManage { 287 | pub fn call(&self, 288 | xws: &XlibWindowSystem, 289 | state: &mut WmState, 290 | config: &Config, 291 | window: Window) { 292 | match *self { 293 | CmdManage::Move(index) => { 294 | debug!("CmdManage::Move: {}, {}", window, index); 295 | state.add_window(Some(index - 1), xws, config, window); 296 | } 297 | CmdManage::Float => { 298 | debug!("CmdManage::Float"); 299 | unimplemented!() 300 | } 301 | CmdManage::Fullscreen => { 302 | debug!("CmdManage::Fullscreen"); 303 | unimplemented!() 304 | } 305 | CmdManage::Ignore => { 306 | debug!("CmdManage::Ignore"); 307 | unimplemented!() 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use crate::keycode::*; 4 | use crate::workspace::WorkspaceConfig; 5 | use crate::state::WmState; 6 | use crate::xlib_window_system::XlibWindowSystem; 7 | use crate::commands::{Cmd, ManageHook}; 8 | use crate::statusbar::Statusbar; 9 | use crate::layout::*; 10 | use std::env; 11 | use std::iter::FromIterator; 12 | use std::default::Default; 13 | use std::io::{self, Write}; 14 | use std::path::{Path, PathBuf}; 15 | use std::fs::{self, File}; 16 | use std::process::{Command, Child, Stdio}; 17 | use std::collections::HashMap; 18 | use libloading::os::unix::{Library, Symbol}; 19 | use anyhow::{bail, Context, Result}; 20 | 21 | pub struct WorkspaceConfigList(Vec); 22 | 23 | impl Default for WorkspaceConfigList { 24 | fn default() -> WorkspaceConfigList { 25 | (1usize..10) 26 | .map(|idx| { 27 | WorkspaceConfig { 28 | tag: idx.to_string(), 29 | screen: 0, 30 | layout: Strut::new(Tall::new(1, 0.5, 0.05)), 31 | } 32 | }) 33 | .collect::>() 34 | .into() 35 | } 36 | } 37 | 38 | impl From> for WorkspaceConfigList { 39 | fn from(list: Vec) -> Self { 40 | WorkspaceConfigList(list) 41 | } 42 | } 43 | 44 | pub struct WorkspaceInfo { 45 | pub id: usize, 46 | pub tag: String, 47 | pub screen: usize, 48 | pub current: bool, 49 | pub visible: bool, 50 | pub urgent: bool, 51 | } 52 | 53 | pub struct PagerInfo { 54 | pub workspaces: Vec, 55 | pub layout_names: Vec, 56 | pub window_title: String, 57 | } 58 | 59 | pub struct Config { 60 | pub path: PathBuf, 61 | pub mod_key: u8, 62 | pub border_width: u32, 63 | pub border_color: u32, 64 | pub border_focus_color: u32, 65 | pub border_urgent_color: u32, 66 | pub greedy_view: bool, 67 | pub terminal: String, 68 | pub keybindings: HashMap, 69 | pub manage_hooks: Vec, 70 | pub statusbar: Option, 71 | } 72 | 73 | impl Default for Config { 74 | fn default() -> Config { 75 | let mut config = Config { 76 | path: PathBuf::from(env!("CARGO_MANIFEST_DIR")), 77 | mod_key: MOD_4, 78 | border_width: 2, 79 | border_color: 0x002e_2e2e, 80 | border_focus_color: 0x002a_82e6, 81 | border_urgent_color: 0x00ff_0000, 82 | greedy_view: false, 83 | terminal: "xterm".to_string(), 84 | keybindings: vec![( 85 | Keybinding { 86 | mods: 0, 87 | key: "Return".to_string() 88 | }, 89 | Cmd::SpawnTerminal(vec![]) 90 | ),( 91 | Keybinding { 92 | mods: 0, 93 | key: "d".to_string(), 94 | }, 95 | Cmd::Exec("dmenu_run".to_string(), vec![]) 96 | ),( 97 | Keybinding { 98 | mods: MOD_SHIFT, 99 | key: "q".to_string(), 100 | }, 101 | Cmd::KillClient 102 | ),( 103 | Keybinding { 104 | mods: 0, 105 | key: "j".to_string(), 106 | }, 107 | Cmd::FocusDown 108 | ),( 109 | Keybinding { 110 | mods: 0, 111 | key: "k".to_string(), 112 | }, 113 | Cmd::FocusUp 114 | ),( 115 | Keybinding { 116 | mods: 0, 117 | key: "m".to_string(), 118 | }, 119 | Cmd::FocusMaster 120 | ),( 121 | Keybinding { 122 | mods: 0, 123 | key: "u".to_string(), 124 | }, 125 | Cmd::FocusParentDown 126 | ),( 127 | Keybinding { 128 | mods: 0, 129 | key: "i".to_string(), 130 | }, 131 | Cmd::FocusParentUp 132 | ),( 133 | Keybinding { 134 | mods: MOD_CONTROL, 135 | key: "m".to_string(), 136 | }, 137 | Cmd::FocusParentMaster 138 | ),( 139 | Keybinding { 140 | mods: MOD_SHIFT, 141 | key: "j".to_string(), 142 | }, 143 | Cmd::SwapDown 144 | ),( 145 | Keybinding { 146 | mods: MOD_SHIFT, 147 | key: "k".to_string(), 148 | }, 149 | Cmd::SwapUp 150 | ),( 151 | Keybinding { 152 | mods: MOD_SHIFT, 153 | key: "Return".to_string(), 154 | }, 155 | Cmd::SwapMaster 156 | ),( 157 | Keybinding { 158 | mods: MOD_SHIFT, 159 | key: "u".to_string(), 160 | }, 161 | Cmd::SwapParentDown 162 | ),( 163 | Keybinding { 164 | mods: MOD_SHIFT, 165 | key: "i".to_string(), 166 | }, 167 | Cmd::SwapParentUp 168 | ),( 169 | Keybinding { 170 | mods: 0, 171 | key: "comma".to_string(), 172 | }, 173 | Cmd::SendLayoutMsg(LayoutMsg::IncreaseMaster) 174 | ),( 175 | Keybinding { 176 | mods: 0, 177 | key: "period".to_string() 178 | }, 179 | Cmd::SendLayoutMsg(LayoutMsg::DecreaseMaster) 180 | ),( 181 | Keybinding { 182 | mods: 0, 183 | key: "l".to_string() 184 | }, 185 | Cmd::SendLayoutMsg(LayoutMsg::Increase) 186 | ),( 187 | Keybinding { 188 | mods: 0, 189 | key: "h".to_string() 190 | }, 191 | Cmd::SendLayoutMsg(LayoutMsg::Decrease) 192 | ),( 193 | Keybinding { 194 | mods: 0, 195 | key: "space".to_string() 196 | }, 197 | Cmd::SendLayoutMsg(LayoutMsg::NextLayout) 198 | ),( 199 | Keybinding { 200 | mods: MOD_SHIFT, 201 | key: "space".to_string() 202 | }, 203 | Cmd::SendLayoutMsg(LayoutMsg::PrevLayout) 204 | ),( 205 | Keybinding { 206 | mods: MOD_SHIFT, 207 | key: "a".to_string() 208 | }, 209 | Cmd::RemoveNested 210 | ),( 211 | Keybinding { 212 | mods: 0, 213 | key: "v".to_string(), 214 | }, 215 | Cmd::NestLayout(Box::new(Vertical::new)) 216 | ),( 217 | Keybinding { 218 | mods: 0, 219 | key: "b".to_string(), 220 | }, 221 | Cmd::NestLayout(Box::new(Horizontal::new)) 222 | ),( 223 | Keybinding { 224 | mods: MOD_SHIFT, 225 | key: "c".to_string(), 226 | }, 227 | Cmd::Exit 228 | ),( 229 | Keybinding { 230 | mods: MOD_SHIFT, 231 | key: "x".to_string(), 232 | }, 233 | Cmd::Reload(vec![]) 234 | )] 235 | .drain(0..).collect(), 236 | manage_hooks: Vec::new(), 237 | statusbar: None, 238 | }; 239 | 240 | for i in 1..10 { 241 | config.keybindings.insert(Keybinding { 242 | mods: 0, 243 | key: i.to_string(), 244 | }, 245 | Cmd::SwitchWorkspace(i) 246 | ); 247 | 248 | config.keybindings.insert(Keybinding { 249 | mods: MOD_SHIFT, 250 | key: i.to_string() 251 | }, 252 | Cmd::MoveToWorkspace(i) 253 | ); 254 | } 255 | 256 | for &(i, key) in &[(1, "w"), (2, "e"), (3, "r")] { 257 | config.keybindings.insert(Keybinding { 258 | mods: 0, 259 | key: key.to_string() 260 | }, 261 | Cmd::SwitchScreen(i) 262 | ); 263 | 264 | config.keybindings.insert(Keybinding { 265 | mods: MOD_SHIFT, 266 | key: key.to_string() 267 | }, 268 | Cmd::MoveToScreen(i) 269 | ); 270 | } 271 | 272 | config 273 | } 274 | } 275 | 276 | impl Config { 277 | pub fn get_dir() -> Result { 278 | let mut cfg_path = None; 279 | let mut args = env::args().skip(1); 280 | 281 | while let Some(arg) = args.next() { 282 | match arg.as_str() { 283 | "--config" | "-c" => cfg_path = args.next(), 284 | x if x.starts_with("--config=") => { 285 | cfg_path = x.splitn(2, '=').last().map(|x| x.into()); 286 | }, 287 | _ => (), 288 | } 289 | } 290 | 291 | cfg_path.ok_or(()) 292 | .or_else(|_| { 293 | env::var("XDG_CONFIG_HOME") 294 | .map(|x| format!("{x}/xr3wm")) 295 | .or_else(|_| { 296 | env::var("HOME") 297 | .map(|x| format!("{x}/.config/xr3wm")) 298 | }) 299 | }) 300 | } 301 | 302 | fn create_default_cfg_file(path: &Path) -> Result<()> { 303 | let mut f = File::create(path.join("config.rs")) 304 | .context("failed to create config.rs")?; 305 | 306 | f.write_all(b"#![allow(unused_imports)] 307 | extern crate xr3wm; 308 | 309 | use std::default::Default; 310 | use xr3wm::core::*; 311 | 312 | #[no_mangle] 313 | pub extern fn configure_workspaces() -> Vec { 314 | use layout::*; 315 | 316 | (1usize..10) 317 | .map(|idx| { 318 | WorkspaceConfig { 319 | tag: idx.to_string(), 320 | screen: 0, 321 | layout: Strut::new(Choose::new(vec![Tall::new(1, 0.5, 0.05), Rotate::new(Tall::new(1, 0.5, 0.05)), Full::new(false)])), 322 | } 323 | }) 324 | .collect() 325 | } 326 | 327 | #[no_mangle] 328 | pub extern fn configure_wm() -> Config { 329 | let mut cfg: Config = Default::default(); 330 | 331 | cfg 332 | }") 333 | .context("failed to write default config file") 334 | } 335 | 336 | fn create_cargo_toml_file(path: &Path) -> Result<()> { 337 | let mut f = File::create(path.join("Cargo.toml")) 338 | .context("failed to create Cargo.toml")?; 339 | 340 | f.write_all(b"[package] 341 | name = \"config\" 342 | version = \"0.0.1\" 343 | authors = [\"xr3wm\"] 344 | 345 | [dependencies.xr3wm] 346 | git = \"https://github.com/tsurai/xr3wm.git\" 347 | default-features = true 348 | 349 | [lib] 350 | name = \"config\" 351 | path = \"src/config.rs\" 352 | crate-type = [\"dylib\"]") 353 | .context("failed to write Cargo.toml") 354 | } 355 | 356 | pub fn compile() -> Result<()> { 357 | let cfg_dir = Self::get_dir()?; 358 | let path = Path::new(&cfg_dir).join("src/"); 359 | if !path.exists() { 360 | fs::create_dir_all(&path) 361 | .context("failed to create config build directory")? 362 | } 363 | 364 | if !path.join("config.rs").exists() { 365 | Self::create_default_cfg_file(&path)?; 366 | } 367 | 368 | let path = path.join(".."); 369 | if !path.join("Cargo.toml").exists() { 370 | Self::create_cargo_toml_file(&path)? 371 | } 372 | 373 | let output = Command::new("cargo") 374 | .arg("build") 375 | .current_dir(path) 376 | .output() 377 | .context("failed to execute cargo")?; 378 | 379 | if !output.status.success() { 380 | let stderr_msg = String::from_utf8(output.stderr) 381 | .context("failed to convert cargo stderr to UTF-8")?; 382 | bail!("failed to recompile: {}", stderr_msg); 383 | } 384 | 385 | Ok(()) 386 | } 387 | 388 | pub fn load() -> Result<(Config, Vec)> { 389 | unsafe { 390 | Config::compile() 391 | .context("failed to compile config")?; 392 | 393 | let cfg_path = format!("{}/target/debug/libconfig.so", Self::get_dir()?); 394 | 395 | let lib: Library = Library::open(Some(cfg_path), libc::RTLD_NOW | libc::RTLD_NODELETE) 396 | .context("failed to load libconfig")?; 397 | 398 | let fn_configure_wm: Symbol Config> = lib.get(b"configure_wm") 399 | .context("failed to get symbol")?; 400 | 401 | let fn_configure_ws: Symbol Vec> = lib.get(b"configure_workspaces") 402 | .context("failed to get symbol")?; 403 | 404 | let cfg_wm = fn_configure_wm(); 405 | let cfg_ws = fn_configure_ws(); 406 | 407 | Ok((cfg_wm, cfg_ws)) 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | pub mod core { 5 | pub mod commands { 6 | pub use crate::commands::{Cmd, CmdManage, ManageHook}; 7 | } 8 | 9 | pub mod keycode { 10 | pub use crate::keycode::*; 11 | } 12 | pub use crate::keycode::Keybinding; 13 | 14 | pub mod layout { 15 | pub use crate::layout::*; 16 | } 17 | 18 | pub mod state { 19 | pub use crate::state::WmState; 20 | } 21 | 22 | pub use crate::config::{Config, PagerInfo}; 23 | pub use crate::statusbar::Statusbar; 24 | pub use crate::workspace::WorkspaceConfig; 25 | } 26 | 27 | mod xlib_window_system; 28 | mod config; 29 | mod state; 30 | mod workspace; 31 | mod commands; 32 | mod keycode; 33 | mod stack; 34 | mod statusbar; 35 | mod layout; 36 | mod ewmh; 37 | mod utils; 38 | -------------------------------------------------------------------------------- /src/ewmh.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::state::WmState; 3 | use crate::workspace::Workspace; 4 | use crate::xlib_window_system::XlibWindowSystem; 5 | use std::ffi::CString; 6 | use x11::xlib::*; 7 | 8 | pub const NET_WM_STATE_REMOVE: u64 = 0; 9 | pub const NET_WM_STATE_ADD: u64 = 1; 10 | pub const NET_WM_STATE_TOGGLE: u64 = 2; 11 | 12 | pub fn init_ewmh(xws: &mut XlibWindowSystem) { 13 | debug!("initializing ewmh"); 14 | let root = xws.get_root_window(); 15 | 16 | let atoms = &[ 17 | "_NET_SUPPORTED", 18 | "_NET_ACTIVE_WINDOW", 19 | "_NET_CLIENT_LIST", 20 | "_NET_CURRENT_DESKTOP", 21 | "_NET_DESKTOP_NAMES", 22 | "_NET_DESKTOP_VIEWPORT", 23 | "_NET_NUMBER_OF_DESKTOPS", 24 | "_NET_WM_DESKTOP", 25 | "_NET_SUPPORTING_WM_CHECK", 26 | "_NET_WM_NAME", 27 | "_NET_WM_STATE", 28 | "_NET_WM_STATE_FULLSCREEN", 29 | "_NET_WM_STATE_DEMANDS_ATTENTION", 30 | "_NET_WM_STATE_STICKY", 31 | "_NET_WM_STRUT_PARTIAL", 32 | "_NET_WM_WINDOW_TYPE", 33 | "_NET_WM_WINDOW_TYPE_DOCK", 34 | ]; 35 | 36 | xws.cache_atoms(atoms); 37 | 38 | let atoms: Vec = atoms.iter().map(|x| xws.get_atom(x)).collect(); 39 | xws.change_property(root, "_NET_SUPPORTED", XA_ATOM, PropModeReplace, &atoms); 40 | 41 | let window = xws.create_hidden_window(); 42 | xws.change_property( 43 | root, 44 | "_NET_SUPPORTING_WM_CHECK", 45 | XA_WINDOW, 46 | PropModeReplace, 47 | &[window], 48 | ); 49 | xws.change_property( 50 | window, 51 | "_NET_SUPPORTING_WM_CHECK", 52 | XA_WINDOW, 53 | PropModeReplace, 54 | &[window], 55 | ); 56 | 57 | let wm_name = CString::new("xr3wm").unwrap(); 58 | xws.change_property( 59 | window, 60 | "_NET_WM_NAME", 61 | "UTF8_STRING", 62 | PropModeReplace, 63 | wm_name.as_bytes_with_nul(), 64 | ); 65 | } 66 | 67 | #[allow(dead_code)] 68 | pub fn process_client_message( 69 | state: &mut WmState, 70 | xws: &XlibWindowSystem, 71 | config: &Config, 72 | window: Window, 73 | msg_type: Atom, 74 | msg_data: &[u64], 75 | ) { 76 | match xws.get_atom_name(msg_type).as_str() { 77 | "_NET_ACTIVE_WINDOW" => { 78 | state.focus_window(xws, config, window, true); 79 | } 80 | "_NET_CURRENT_DESKTOP" => { 81 | state.switch_to_ws(xws, config, msg_data[0] as usize, true); 82 | } 83 | "_NET_WM_STATE" => { 84 | trace!("process WM_STATE client message"); 85 | let mode = msg_data[0]; 86 | let wm_states: Vec = msg_data[1..3] 87 | .iter() 88 | .filter(|&x| *x != 0) 89 | .cloned() 90 | .collect(); 91 | 92 | let mut redraw = false; 93 | let ret = set_wm_state(xws, window, &wm_states, mode); 94 | 95 | if let Some((add_states, rem_states)) = ret { 96 | 97 | let fullscreen = xws.get_atom("_NET_WM_STATE_FULLSCREEN"); 98 | let attention = xws.get_atom("_NET_WM_STATE_DEMANDS_ATTENTION"); 99 | let sticky = xws.get_atom("_NET_WM_STATE_STICKY"); 100 | 101 | for s in add_states { 102 | if s == attention { 103 | state.set_urgency(true, window); 104 | redraw = true; 105 | } else if s == sticky { 106 | state.remove_window(xws, config, window); 107 | state.add_unmanaged(window); 108 | redraw = true; 109 | } else if s == fullscreen { 110 | redraw = true; 111 | } 112 | } 113 | 114 | if rem_states.contains(&xws.get_atom("_NET_WM_STATE_DEMANDS_ATTENTION")) { 115 | state.set_urgency(false, window); 116 | redraw = true; 117 | } 118 | 119 | if rem_states 120 | .iter() 121 | .any(|&x| x == xws.get_atom("_NET_WM_STATE_FULLSCREEN")) 122 | { 123 | redraw = true; 124 | } 125 | } 126 | 127 | if redraw { 128 | state.redraw(xws, config); 129 | } 130 | } 131 | _ => {} 132 | } 133 | } 134 | 135 | pub fn set_active_window(xws: &XlibWindowSystem, window: Window) { 136 | trace!("set active window: {:#x}", window); 137 | let root = xws.get_root_window(); 138 | xws.change_property( 139 | root, 140 | "_NET_ACTIVE_WINDOW", 141 | XA_WINDOW, 142 | PropModeReplace, 143 | &[window], 144 | ); 145 | } 146 | 147 | pub fn get_active_window(xws: &XlibWindowSystem) -> Option { 148 | let root = xws.get_root_window(); 149 | xws.get_property(root, "_NET_ACTIVE_WINDOW").map(|x| x[0]) 150 | } 151 | 152 | #[allow(dead_code)] 153 | pub fn set_number_of_desktops(xws: &XlibWindowSystem, num_desktops: usize) { 154 | let root = xws.get_root_window(); 155 | xws.change_property( 156 | root, 157 | "_NET_NUMBER_OF_DESKTOPS", 158 | XA_CARDINAL, 159 | PropModeReplace, 160 | &[num_desktops as u64], 161 | ); 162 | } 163 | 164 | pub fn set_current_desktop(xws: &XlibWindowSystem, index: usize) { 165 | let root = xws.get_root_window(); 166 | xws.change_property( 167 | root, 168 | "_NET_CURRENT_DESKTOP", 169 | XA_CARDINAL, 170 | PropModeReplace, 171 | &[index as u64], 172 | ); 173 | } 174 | 175 | #[allow(dead_code)] 176 | pub fn set_desktop_names(xws: &XlibWindowSystem, workspaces: &[Workspace]) { 177 | let root = xws.get_root_window(); 178 | 179 | let names: Vec> = workspaces 180 | .iter() 181 | .map(|x| x.get_tag().to_owned()) 182 | .filter_map(|x| CString::new(x).ok()) 183 | .map(|x| x.into_bytes_with_nul()) 184 | .collect(); 185 | 186 | xws.change_property( 187 | root, 188 | "_NET_DESKTOP_NAMES", 189 | "UTF8_STRING", 190 | PropModeReplace, 191 | &names.as_slice().concat(), 192 | ); 193 | } 194 | 195 | pub fn set_desktop_viewport(xws: &XlibWindowSystem, workspaces: &[Workspace]) { 196 | let root = xws.get_root_window(); 197 | let screens = xws.get_screen_infos(); 198 | 199 | let viewports: Vec = workspaces 200 | .iter() 201 | .map(|ws| { 202 | screens 203 | .get(ws.get_screen()) 204 | .map(|s| vec![s.x as u64, s.y as u64]) 205 | .unwrap_or_else(|| vec![0u64, 0]) 206 | }) 207 | .collect::>>() 208 | .as_slice() 209 | .concat(); 210 | 211 | xws.change_property( 212 | root, 213 | "_NET_DESKTOP_VIEWPORT", 214 | XA_CARDINAL, 215 | PropModeReplace, 216 | &viewports, 217 | ); 218 | } 219 | 220 | pub fn set_client_list(xws: &XlibWindowSystem, workspaces: &[Workspace]) { 221 | let root = xws.get_root_window(); 222 | 223 | let clients: Vec = workspaces 224 | .iter() 225 | .map(|ws| ws.all()) 226 | .collect::>>() 227 | .as_slice() 228 | .concat(); 229 | 230 | xws.change_property( 231 | root, 232 | "_NET_CLIENT_LIST", 233 | XA_WINDOW, 234 | PropModeReplace, 235 | &clients, 236 | ); 237 | } 238 | 239 | pub fn set_wm_desktop(xws: &XlibWindowSystem, window: Window, idx: usize) { 240 | xws.change_property( 241 | window, 242 | "_NET_WM_DESKTOP", 243 | XA_CARDINAL, 244 | PropModeReplace, 245 | &[idx as u32], 246 | ); 247 | } 248 | 249 | pub fn set_wm_state( 250 | xws: &XlibWindowSystem, 251 | window: Window, 252 | atoms: &[Atom], 253 | mode: u64, 254 | ) -> Option<(Vec, Vec)> { 255 | let active_atoms = xws 256 | .get_property(window, "_NET_WM_STATE") 257 | .unwrap_or_default(); 258 | 259 | match mode { 260 | NET_WM_STATE_REMOVE => { 261 | xws.change_property( 262 | window, 263 | "_NET_WM_STATE", 264 | XA_ATOM, 265 | PropModeReplace, 266 | &active_atoms 267 | .into_iter() 268 | .filter(|x| !atoms.iter().any(|y| y == x)) 269 | .collect::>(), 270 | ); 271 | Some((vec![], atoms.to_vec())) 272 | } 273 | NET_WM_STATE_ADD | NET_WM_STATE_TOGGLE => { 274 | let (add_atoms, rem_atoms) = if mode == NET_WM_STATE_ADD { 275 | (atoms.to_vec(), vec![]) 276 | } else { 277 | atoms 278 | .iter() 279 | .partition::, _>(|x| !active_atoms.iter().any(|y| y == *x)) 280 | }; 281 | 282 | let atoms: Vec = active_atoms 283 | .iter() 284 | .filter(|&x| !rem_atoms.iter().any(|y| y == x)) 285 | .chain(add_atoms.iter()) 286 | .copied() 287 | .collect(); 288 | 289 | xws.change_property(window, "_NET_WM_STATE", XA_ATOM, PropModeReplace, &atoms); 290 | 291 | Some((add_atoms, rem_atoms)) 292 | } 293 | _ => None, 294 | } 295 | } 296 | 297 | pub fn is_window_sticky(xws: &XlibWindowSystem, window: Window) -> bool { 298 | xws.get_property(window, "_NET_WM_STATE") 299 | .map(|prop| { 300 | prop.iter() 301 | .any(|&x| x == xws.get_atom("_NET_WM_STATE_STICKY")) 302 | }) 303 | .unwrap_or(false) 304 | } 305 | 306 | pub fn is_window_fullscreen(xws: &XlibWindowSystem, window: Window) -> bool { 307 | xws.get_property(window, "_NET_WM_STATE") 308 | .map(|prop| { 309 | prop.iter() 310 | .any(|&x| x == xws.get_atom("_NET_WM_STATE_FULLSCREEN")) 311 | }) 312 | .unwrap_or(false) 313 | } 314 | -------------------------------------------------------------------------------- /src/keycode.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub const MOD_SHIFT: u8 = 1; 4 | pub const MOD_LOCK: u8 = 1 << 1; 5 | pub const MOD_CONTROL: u8 = 1 << 2; 6 | pub const MOD_1: u8 = 1 << 3; 7 | pub const MOD_2: u8 = 1 << 4; 8 | pub const MOD_3: u8 = 1 << 5; 9 | pub const MOD_4: u8 = 1 << 6; 10 | pub const MOD_5: u8 = 1 << 7; 11 | 12 | #[derive(PartialEq, Hash)] 13 | pub struct Keybinding { 14 | pub mods: u8, 15 | pub key: String, 16 | } 17 | 18 | impl Eq for Keybinding {} 19 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(clippy::new_ret_no_self)] 3 | 4 | use crate::stack::{Node, Stack}; 5 | use crate::xlib_window_system::XlibWindowSystem; 6 | use crate::ewmh; 7 | use std::cmp::min; 8 | use std::fmt; 9 | use x11::xlib::Window; 10 | 11 | #[cfg(feature = "reload")] 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[derive(Clone, Copy)] 15 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 16 | pub struct Rect { 17 | pub x: u32, 18 | pub y: u32, 19 | pub width: u32, 20 | pub height: u32, 21 | } 22 | 23 | impl fmt::Debug for Rect { 24 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 25 | write!(f, 26 | "{{ x: {}, y: {}, width: {}, height: {} }}", 27 | self.x, 28 | self.y, 29 | self.width, 30 | self.height) 31 | } 32 | } 33 | 34 | #[derive(Clone, PartialEq, Eq)] 35 | pub enum LayoutMsg { 36 | Increase, 37 | Decrease, 38 | IncreaseMaster, 39 | DecreaseMaster, 40 | NextLayout, 41 | PrevLayout, 42 | FirstLayout, 43 | LastLayout, 44 | ResetLayout, 45 | NthLayout(usize), 46 | Custom(String), 47 | } 48 | 49 | impl fmt::Debug for LayoutMsg { 50 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 51 | match *self { 52 | LayoutMsg::Increase => write!(f, "Increase"), 53 | LayoutMsg::Decrease => write!(f, "Decrease"), 54 | LayoutMsg::IncreaseMaster => write!(f, "IncreasMaster"), 55 | LayoutMsg::DecreaseMaster => write!(f, "DecreaseMaster"), 56 | LayoutMsg::NextLayout => write!(f, "NextLayout"), 57 | LayoutMsg::PrevLayout => write!(f, "PrevLayout"), 58 | LayoutMsg::FirstLayout => write!(f, "FirstLayout"), 59 | LayoutMsg::NthLayout(n) => write!(f, "NthLayout: {n}"), 60 | LayoutMsg::LastLayout => write!(f, "LastLayout"), 61 | LayoutMsg::ResetLayout => write!(f, "ResetLayout"), 62 | LayoutMsg::Custom(ref val) => write!(f, "Custom({})", val.clone()), 63 | } 64 | } 65 | } 66 | 67 | #[cfg_attr(feature = "reload", typetag::serde(tag = "type"))] 68 | pub trait Layout { 69 | fn name(&self) -> String; 70 | fn send_msg(&mut self, xws: &XlibWindowSystem, nodes: &[Node], msg: LayoutMsg); 71 | 72 | fn apply(&self, area: Rect, _: &XlibWindowSystem, stack: &Stack) -> Vec { 73 | self.simple_apply(area, &stack.nodes) 74 | } 75 | 76 | fn simple_apply(&self, _: Rect, _: &[Node]) -> Vec { 77 | Vec::new() 78 | } 79 | } 80 | 81 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 82 | pub struct Choose { 83 | layouts: Vec>, 84 | current: usize, 85 | } 86 | 87 | impl Choose { 88 | pub fn new(layouts: Vec>) -> Box { 89 | // TODO: add proper error handling 90 | if layouts.is_empty() { 91 | panic!("Choose layout needs at least one layout"); 92 | } 93 | 94 | Box::new(Choose { 95 | layouts, 96 | current: 0, 97 | }) 98 | } 99 | } 100 | 101 | #[cfg_attr(feature = "reload", typetag::serde)] 102 | impl Layout for Choose { 103 | fn name(&self) -> String { 104 | self.layouts[self.current].name() 105 | } 106 | 107 | fn send_msg(&mut self, xws: &XlibWindowSystem, nodes: &[Node], msg: LayoutMsg) { 108 | let len = self.layouts.len(); 109 | let prev = self.current; 110 | 111 | match msg { 112 | LayoutMsg::NextLayout => { 113 | self.current = (self.current + 1) % len; 114 | } 115 | LayoutMsg::PrevLayout => { 116 | self.current = (self.current + len - 1) % len; 117 | } 118 | LayoutMsg::FirstLayout => { 119 | self.current = 0; 120 | } 121 | LayoutMsg::LastLayout => { 122 | self.current = len - 1; 123 | } 124 | LayoutMsg::NthLayout(n) => { 125 | if n < self.layouts.len() { 126 | self.current = n 127 | } 128 | } 129 | x => { 130 | self.layouts[self.current].send_msg(xws, nodes, x); 131 | } 132 | } 133 | 134 | if self.current != prev { 135 | self.layouts[prev].send_msg(xws, nodes, LayoutMsg::ResetLayout); 136 | } 137 | } 138 | 139 | fn apply(&self, area: Rect, xws: &XlibWindowSystem, stack: &Stack) -> Vec { 140 | self.layouts[self.current].apply(area, xws, stack) 141 | } 142 | } 143 | 144 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 145 | pub struct Tall { 146 | num_masters: usize, 147 | ratio: f32, 148 | ratio_increment: f32, 149 | } 150 | 151 | impl Tall { 152 | pub fn new(num_masters: usize, ratio: f32, ratio_increment: f32) -> Box { 153 | Box::new(Tall { 154 | num_masters, 155 | ratio, 156 | ratio_increment, 157 | }) 158 | } 159 | } 160 | 161 | #[cfg_attr(feature = "reload", typetag::serde)] 162 | impl Layout for Tall { 163 | fn name(&self) -> String { 164 | "Tall".to_string() 165 | } 166 | 167 | fn send_msg(&mut self, _: &XlibWindowSystem, _: &[Node], msg: LayoutMsg) { 168 | match msg { 169 | LayoutMsg::Increase => { 170 | if self.ratio + self.ratio_increment < 1.0 { 171 | self.ratio += self.ratio_increment; 172 | } 173 | } 174 | LayoutMsg::Decrease => { 175 | if self.ratio - self.ratio_increment > self.ratio_increment { 176 | self.ratio -= self.ratio_increment; 177 | } 178 | } 179 | LayoutMsg::IncreaseMaster => self.num_masters += 1, 180 | LayoutMsg::DecreaseMaster => { 181 | if self.num_masters > 1 { 182 | self.num_masters -= 1; 183 | } 184 | } 185 | _ => {} 186 | } 187 | } 188 | 189 | fn simple_apply(&self, area: Rect, windows: &[Node]) -> Vec { 190 | let nwindows = windows.len(); 191 | 192 | (0..nwindows) 193 | .map(|i| { 194 | if i < self.num_masters { 195 | let yoff = area.height / min(self.num_masters, nwindows) as u32; 196 | 197 | Rect { 198 | x: area.x, 199 | y: area.y + (yoff * i as u32), 200 | width: (area.width as f32 * 201 | (1.0 - 202 | (nwindows > self.num_masters) as u32 as f32 * 203 | (1.0 - self.ratio))) 204 | .floor() as u32, 205 | height: yoff, 206 | } 207 | } else { 208 | let yoff = area.height / (nwindows - self.num_masters) as u32; 209 | 210 | Rect { 211 | x: area.x + (area.width as f32 * self.ratio).floor() as u32, 212 | y: area.y + (yoff * (i - self.num_masters) as u32), 213 | width: (area.width as f32 * (1.0 - self.ratio)).floor() as u32, 214 | height: yoff, 215 | } 216 | } 217 | }) 218 | .collect() 219 | } 220 | } 221 | 222 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 223 | pub struct Strut { 224 | layout: Box, 225 | } 226 | 227 | impl Strut { 228 | pub fn new(layout: Box) -> Box { 229 | Box::new(Strut { 230 | layout 231 | }) 232 | } 233 | } 234 | 235 | #[cfg_attr(feature = "reload", typetag::serde)] 236 | impl Layout for Strut { 237 | fn name(&self) -> String { 238 | self.layout.name() 239 | } 240 | 241 | fn send_msg(&mut self, xws: &XlibWindowSystem, nodes: &[Node], msg: LayoutMsg) { 242 | self.layout.send_msg(xws, nodes, msg); 243 | } 244 | 245 | fn apply(&self, area: Rect, xws: &XlibWindowSystem, stack: &Stack) -> Vec { 246 | let mut new_area = Rect { 247 | x: 0, 248 | y: 0, 249 | width: 0, 250 | height: 0, 251 | }; 252 | let strut = xws.compute_struts(area); 253 | 254 | new_area.x = area.x + strut.0; 255 | new_area.width = area.width - (strut.0 + strut.1); 256 | new_area.y = area.y + strut.2; 257 | new_area.height = area.height - (strut.2 + strut.3); 258 | 259 | self.layout.apply(new_area, xws, stack) 260 | } 261 | } 262 | 263 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 264 | pub struct Full { 265 | focus: Option, 266 | is_fullscreen: bool, 267 | } 268 | 269 | impl Full { 270 | pub fn new(is_fullscreen: bool) -> Box { 271 | Box::new(Full{ 272 | focus: None, 273 | is_fullscreen, 274 | }) 275 | } 276 | 277 | fn reset(xws: &XlibWindowSystem, nodes: &[Node]) { 278 | nodes.iter() 279 | .filter_map(|x| if let Node::Window(w) = x { Some(w) } else { None }) 280 | .for_each(|&w| { 281 | ewmh::set_wm_state(xws, w, &[xws.get_atom("_NET_WM_STATE_FULLSCREEN")], ewmh::NET_WM_STATE_REMOVE); 282 | }); 283 | } 284 | } 285 | 286 | #[cfg_attr(feature = "reload", typetag::serde)] 287 | impl Layout for Full { 288 | fn name(&self) -> String { 289 | "Full".to_string() 290 | } 291 | 292 | fn send_msg(&mut self, xws: &XlibWindowSystem, nodes: &[Node], msg: LayoutMsg) { 293 | match msg { 294 | LayoutMsg::ResetLayout => { 295 | Full::reset(xws, nodes); 296 | }, 297 | LayoutMsg::Custom(x) if x.as_str() == "ToggleFullscreen" => { 298 | self.is_fullscreen = !self.is_fullscreen; 299 | if !self.is_fullscreen { 300 | Full::reset(xws, nodes); 301 | } 302 | }, 303 | _ => (), 304 | } 305 | } 306 | 307 | fn apply(&self, area: Rect, xws: &XlibWindowSystem, stack: &Stack) -> Vec { 308 | let area = if self.is_fullscreen { 309 | xws.get_screen_infos() 310 | .into_iter() 311 | .find(|screen| area.x >= screen.x && area.x < screen.x + screen.width && area.y >= screen.y && area.y < screen.x + screen.width) 312 | .unwrap_or(area) 313 | } else { 314 | area 315 | }; 316 | 317 | stack.nodes.iter() 318 | .enumerate() 319 | .map(|(i,node)| { 320 | match node { 321 | Node::Window(w) => { 322 | let is_active = ewmh::get_active_window(xws) == Some(*w); 323 | let is_focused = Some(i) == stack.focus; 324 | 325 | if self.is_fullscreen { 326 | ewmh::set_wm_state(xws, *w, &[xws.get_atom("_NET_WM_STATE_FULLSCREEN")], (is_active && is_focused) as u64); 327 | } 328 | 329 | if is_active && is_focused { 330 | xws.raise_window(*w); 331 | } 332 | }, 333 | Node::Stack(s) => { 334 | s.all_windows().iter().for_each(|&w| { 335 | if Some(i) == stack.focus { 336 | xws.raise_window(w); 337 | } 338 | }); 339 | } 340 | } 341 | 342 | Rect { 343 | x: area.x, 344 | y: area.y, 345 | width: area.width, 346 | height: area.height, 347 | } 348 | }).collect() 349 | } 350 | } 351 | 352 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 353 | pub struct Gap { 354 | screen_gap: u32, 355 | window_gap: u32, 356 | layout: Box, 357 | } 358 | 359 | impl Gap { 360 | pub fn new(screen_gap: u32, window_gap: u32, layout: Box) -> Box { 361 | Box::new(Gap { 362 | screen_gap, 363 | window_gap, 364 | layout, 365 | }) 366 | } 367 | } 368 | 369 | #[cfg_attr(feature = "reload", typetag::serde)] 370 | impl Layout for Gap { 371 | fn name(&self) -> String { 372 | self.layout.name() 373 | } 374 | 375 | fn send_msg(&mut self, xws: &XlibWindowSystem, nodes: &[Node], msg: LayoutMsg) { 376 | self.layout.send_msg(xws, nodes, msg); 377 | } 378 | 379 | fn apply(&self, area: Rect, xws: &XlibWindowSystem, stack: &Stack) -> Vec { 380 | let area = Rect { 381 | x: area.x + self.screen_gap, 382 | y: area.y + self.screen_gap, 383 | width: area.width - (2 * self.screen_gap), 384 | height: area.height - (2 * self.screen_gap), 385 | }; 386 | 387 | let mut rects = self.layout.apply(area, xws, stack); 388 | 389 | for rect in rects.iter_mut() { 390 | rect.x += self.window_gap; 391 | rect.y += self.window_gap; 392 | rect.width -= 2 * self.window_gap; 393 | rect.height -= 2 * self.window_gap; 394 | } 395 | 396 | rects 397 | } 398 | } 399 | 400 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 401 | pub enum MirrorStyle { 402 | Horizontal, 403 | Vertical, 404 | } 405 | 406 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 407 | pub struct Mirror { 408 | style: MirrorStyle, 409 | layout: Box, 410 | } 411 | 412 | impl Mirror { 413 | pub fn new(style: MirrorStyle, layout: Box) -> Box { 414 | Box::new(Mirror { 415 | style, 416 | layout 417 | }) 418 | } 419 | } 420 | 421 | #[cfg_attr(feature = "reload", typetag::serde)] 422 | impl Layout for Mirror { 423 | fn name(&self) -> String { 424 | format!("Mirror({})", self.layout.name()) 425 | } 426 | 427 | fn send_msg(&mut self, xws: &XlibWindowSystem, nodes: &[Node], msg: LayoutMsg) { 428 | self.layout.send_msg(xws, nodes, msg); 429 | } 430 | 431 | fn apply(&self, area: Rect, xws: &XlibWindowSystem, stack: &Stack) -> Vec { 432 | let mut rects = self.layout.apply(area, xws, stack); 433 | 434 | match self.style { 435 | MirrorStyle::Horizontal => { 436 | rects.iter_mut().for_each(|r| r.y = area.y + area.height - min(r.y + r.height - area.y, area.height)) 437 | } 438 | MirrorStyle::Vertical => { 439 | rects.iter_mut().for_each(|r| r.x = area.width - min(r.x + r.width, area.width)) 440 | } 441 | } 442 | 443 | rects 444 | } 445 | } 446 | 447 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 448 | pub struct Rotate { 449 | layout: Box, 450 | } 451 | 452 | impl Rotate { 453 | pub fn new(layout: Box) -> Box { 454 | Box::new(Rotate { 455 | layout 456 | }) 457 | } 458 | 459 | fn rotate_rect(rect: Rect) -> Rect { 460 | Rect { 461 | x: rect.y, 462 | y: rect.x, 463 | width: rect.height, 464 | height: rect.width, 465 | } 466 | } 467 | } 468 | 469 | #[cfg_attr(feature = "reload", typetag::serde)] 470 | impl Layout for Rotate { 471 | fn name(&self) -> String { 472 | format!("Rotate({})", self.layout.name()) 473 | } 474 | 475 | fn send_msg(&mut self, xws: &XlibWindowSystem, nodes: &[Node], msg: LayoutMsg) { 476 | self.layout.send_msg(xws, nodes, msg); 477 | } 478 | 479 | fn apply(&self, area: Rect, xws: &XlibWindowSystem, stack: &Stack) -> Vec { 480 | self.layout 481 | .apply(Self::rotate_rect(area), xws, stack) 482 | .iter() 483 | .map(|&r| { 484 | Self::rotate_rect(r) 485 | }) 486 | .collect() 487 | } 488 | } 489 | 490 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 491 | pub struct Horizontal; 492 | 493 | impl Horizontal { 494 | pub fn new() -> Box { 495 | Box::new(Horizontal {}) 496 | } 497 | } 498 | 499 | #[cfg_attr(feature = "reload", typetag::serde)] 500 | impl Layout for Horizontal { 501 | fn name(&self) -> String { 502 | "Horizontal".to_string() 503 | } 504 | 505 | fn send_msg(&mut self, _: &XlibWindowSystem, _: &[Node], _: LayoutMsg) {} 506 | 507 | fn simple_apply(&self, area: Rect, windows: &[Node]) -> Vec { 508 | let nwindows = windows.len(); 509 | 510 | (0..nwindows) 511 | .map(|i| { 512 | let yoff = area.height / nwindows as u32; 513 | 514 | Rect { 515 | x: area.x, 516 | y: area.y + (yoff * i as u32), 517 | width: area.width, 518 | height: yoff, 519 | } 520 | }) 521 | .collect() 522 | } 523 | } 524 | 525 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 526 | pub struct Vertical; 527 | 528 | impl Vertical { 529 | pub fn new() -> Box { 530 | Box::new(Vertical {}) 531 | } 532 | } 533 | 534 | #[cfg_attr(feature = "reload", typetag::serde)] 535 | impl Layout for Vertical { 536 | fn name(&self) -> String { 537 | "Vertical".to_string() 538 | } 539 | 540 | fn send_msg(&mut self, _: &XlibWindowSystem, _: &[Node], _: LayoutMsg) {} 541 | 542 | fn simple_apply(&self, area: Rect, windows: &[Node]) -> Vec { 543 | let nwindows = windows.len(); 544 | 545 | (0..nwindows) 546 | .map(|i| { 547 | let xoff = area.width / nwindows as u32; 548 | 549 | Rect { 550 | x: area.x + (xoff * i as u32), 551 | y: area.y, 552 | width: xoff, 553 | height: area.height, 554 | } 555 | }) 556 | .collect() 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /src/stack.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use crate::layout::{Layout, LayoutMsg, Rect}; 4 | use crate::workspace::MoveOp; 5 | use crate::xlib_window_system::XlibWindowSystem; 6 | use anyhow::{anyhow, Context, Result}; 7 | use std::cmp; 8 | use x11::xlib::Window; 9 | 10 | #[cfg(feature = "reload")] 11 | use serde::{Deserialize, Serialize}; 12 | 13 | pub struct LayoutIter<'a> { 14 | stack: Option<&'a Stack>, 15 | } 16 | 17 | impl<'a> Iterator for LayoutIter<'a> { 18 | type Item = &'a Box; 19 | 20 | fn next(&mut self) -> Option { 21 | if let Some(stack) = self.stack { 22 | let layout = stack.layout.as_ref(); 23 | 24 | let next_stack = stack.focus.and_then(|idx| match stack.nodes.get(idx) { 25 | Some(Node::Stack(s)) => Some(s), 26 | _ => None, 27 | }); 28 | 29 | *self = LayoutIter { stack: next_stack }; 30 | 31 | layout 32 | } else { 33 | None 34 | } 35 | } 36 | } 37 | 38 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 39 | pub enum Node { 40 | Window(Window), 41 | Stack(Stack), 42 | } 43 | 44 | #[derive(Default)] 45 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 46 | pub struct Stack { 47 | pub layout: Option>, 48 | pub focus: Option, 49 | pub nodes: Vec, 50 | pub urgent: Vec, 51 | } 52 | 53 | impl Stack { 54 | pub fn new(layout: Option>) -> Self { 55 | Self { 56 | layout, 57 | focus: None, 58 | nodes: Vec::new(), 59 | urgent: Vec::new(), 60 | } 61 | } 62 | 63 | pub fn serialize(&self) -> String { 64 | format!( 65 | "{}:{}", 66 | self.all_windows() 67 | .iter() 68 | .map(|&x| x.to_string()) 69 | .collect::>() 70 | .join(","), 71 | self.focus.unwrap_or(0) 72 | ) 73 | } 74 | 75 | pub fn deserialize<'a, I: Iterator>( 76 | data_iter: &mut I, 77 | windows: &[Window], 78 | ) -> Result { 79 | let nodes: Vec = data_iter 80 | .next() 81 | .map(|x| { 82 | x.split(',') 83 | .filter_map(|w| w.parse::().ok()) 84 | // filter obsolete windows that no longer exist 85 | .filter(|w| windows.contains(w)) 86 | .map(Node::Window) 87 | .collect() 88 | }) 89 | .unwrap_or_default(); 90 | 91 | let focus = data_iter 92 | .next() 93 | .ok_or_else(|| anyhow!("missing stack focus data"))? 94 | .parse::() 95 | .map(|x| { 96 | if nodes.is_empty() { 97 | None 98 | } else { 99 | Some(cmp::min(nodes.len() - 1, x)) 100 | } 101 | }) 102 | .context("failed to parse stack focus value")?; 103 | 104 | Ok(Self { 105 | focus, 106 | nodes, 107 | ..Default::default() 108 | }) 109 | } 110 | 111 | pub fn all(&self) -> Vec<&Node> { 112 | self.nodes.iter().collect() 113 | } 114 | 115 | #[inline] 116 | pub fn len(&self) -> usize { 117 | self.nodes.len() 118 | } 119 | 120 | fn windows_idx(&self) -> Vec<(usize, Window)> { 121 | self.nodes 122 | .iter() 123 | .enumerate() 124 | .filter_map(|(i, x)| { 125 | if let Node::Window(w) = x { 126 | Some((i, *w)) 127 | } else { 128 | None 129 | } 130 | }) 131 | .collect() 132 | } 133 | 134 | fn windows(&self) -> Vec { 135 | self.nodes 136 | .iter() 137 | .filter_map(|x| { 138 | if let Node::Window(w) = x { 139 | Some(*w) 140 | } else { 141 | None 142 | } 143 | }) 144 | .collect() 145 | } 146 | 147 | pub fn all_windows(&self) -> Vec { 148 | self.windows() 149 | .into_iter() 150 | .chain(self.all_stacks().iter().flat_map(|s| s.all_windows())) 151 | .collect() 152 | } 153 | 154 | pub fn all_stacks(&self) -> Vec<&Stack> { 155 | self.nodes 156 | .iter() 157 | .filter_map(|x| { 158 | if let Node::Stack(s) = x { 159 | Some(s) 160 | } else { 161 | None 162 | } 163 | }) 164 | .collect() 165 | } 166 | 167 | pub fn all_stacks_mut(&mut self) -> Vec<&mut Stack> { 168 | self.nodes 169 | .iter_mut() 170 | .filter_map(|x| { 171 | if let Node::Stack(s) = x { 172 | Some(s) 173 | } else { 174 | None 175 | } 176 | }) 177 | .collect() 178 | } 179 | 180 | pub fn layout_iter(&self) -> LayoutIter { 181 | LayoutIter { stack: Some(self) } 182 | } 183 | 184 | pub fn focused_window(&self) -> Option { 185 | match self.focused_node() { 186 | Some(Node::Stack(s)) => s.focused_window(), 187 | Some(Node::Window(w)) => Some(*w), 188 | _ => None, 189 | } 190 | } 191 | 192 | fn focused_node(&self) -> Option<&Node> { 193 | self.focus.and_then(|idx| self.nodes.get(idx)) 194 | } 195 | 196 | fn focused_node_mut(&mut self) -> Option<&mut Node> { 197 | self.focus.and_then(|idx| self.nodes.get_mut(idx)) 198 | } 199 | 200 | pub fn focused_stack_mut(&mut self) -> Option<&mut Stack> { 201 | if let Some(Node::Stack(s)) = self.focused_node_mut() { 202 | return if matches!(s.focused_node(), Some(Node::Window(_))) { 203 | Some(s) 204 | } else { 205 | s.focused_stack_mut() 206 | } 207 | } 208 | None 209 | } 210 | 211 | pub fn focus_window(&mut self, window: Window) -> bool { 212 | let idx = (0..self.len()).find(|i| match self.nodes.get_mut(*i) { 213 | Some(Node::Window(w)) => *w == window, 214 | Some(Node::Stack(s)) => s.focus_window(window), 215 | _ => false, 216 | }); 217 | 218 | if idx.is_some() { 219 | self.focus = idx; 220 | } 221 | 222 | idx.is_some() 223 | } 224 | 225 | pub fn move_parent_focus(&mut self, op: MoveOp) -> Option { 226 | match self.focused_node_mut() { 227 | Some(Node::Stack(s)) => { 228 | if matches!(s.focused_node(), Some(Node::Window(_))) { 229 | let idx = self.focus.unwrap_or(0); 230 | 231 | self.focus = Some(match op { 232 | MoveOp::Down => (idx + 1) % self.len(), 233 | MoveOp::Up => idx.checked_sub(1).unwrap_or_else(|| self.len() - 1), 234 | MoveOp::Swap => 0, 235 | }); 236 | self.focused_window() 237 | } else { 238 | s.move_parent_focus(op) 239 | } 240 | } 241 | Some(Node::Window(_)) => None, 242 | None => None, 243 | } 244 | } 245 | 246 | pub fn move_focus(&mut self, op: MoveOp) -> Option { 247 | match self.focused_node_mut() { 248 | Some(Node::Stack(s)) => s.move_focus(op), 249 | Some(Node::Window(_)) => { 250 | let idx = self.focus.unwrap_or(0); 251 | 252 | self.focus = Some(match op { 253 | MoveOp::Down => (idx + 1) % self.len(), 254 | MoveOp::Up => idx.checked_sub(1).unwrap_or_else(|| self.len() - 1), 255 | MoveOp::Swap => 0, 256 | }); 257 | self.focused_window() 258 | } 259 | None => None, 260 | } 261 | } 262 | 263 | pub fn contains(&self, window: Window) -> bool { 264 | self.all_windows().iter().any(|&x| x == window) 265 | } 266 | 267 | pub fn is_urgent(&self) -> bool { 268 | !self.urgent.is_empty() || self.all_stacks().iter().any(|s| s.is_urgent()) 269 | } 270 | 271 | pub fn add_window(&mut self, window: Window) { 272 | match self.focused_node_mut() { 273 | Some(Node::Stack(s)) => s.add_window(window), 274 | Some(Node::Window(_)) => self 275 | .nodes 276 | .insert(self.focus.expect("focus idx") + 1, Node::Window(window)), 277 | None => self.nodes.push(Node::Window(window)), 278 | } 279 | } 280 | 281 | pub fn add_container(&mut self, layout: Box) { 282 | if self.len() == 0 { 283 | let stack = Stack::new(Some(layout)); 284 | self.nodes.push(Node::Stack(stack)); 285 | self.focus = Some(0); 286 | } else { 287 | match self.focused_node_mut() { 288 | Some(Node::Stack(s)) => { 289 | if s.len() == 1 { 290 | s.layout = Some(layout) 291 | } else { 292 | s.add_container(layout) 293 | } 294 | } 295 | Some(Node::Window(w)) => { 296 | let mut stack = Stack::new(Some(layout)); 297 | stack.add_window(*w); 298 | stack.focus = Some(0); 299 | self.nodes[self.focus.unwrap_or(0)] = Node::Stack(stack) 300 | } 301 | _ => (), 302 | } 303 | } 304 | } 305 | 306 | pub fn dissolve_container(&mut self) { 307 | if let Some(Node::Stack(s)) = self.focused_node_mut() { 308 | if matches!(s.focused_node(), Some(Node::Window(_))) { 309 | let idx = self.focus.unwrap_or(0); 310 | if let Node::Stack(stack) = self.nodes.remove(idx) { 311 | let prev_focus = stack.focus.unwrap_or(0); 312 | for node in stack.nodes.into_iter().rev() { 313 | self.nodes.insert(idx, node); 314 | } 315 | self.focus = Some(idx + prev_focus); 316 | } 317 | } else { 318 | s.dissolve_container() 319 | } 320 | } 321 | } 322 | 323 | pub fn move_window(&mut self, op: MoveOp) { 324 | match self.focused_node_mut() { 325 | Some(Node::Stack(s)) => s.move_window(op), 326 | _ => { 327 | let idx = self.focus.unwrap_or(0); 328 | 329 | self.focus = Some(match op { 330 | MoveOp::Down => (idx + 1) % self.nodes.len(), 331 | MoveOp::Up => (idx + self.nodes.len() - 1) % self.nodes.len(), 332 | MoveOp::Swap => 0, 333 | }); 334 | 335 | self.nodes.swap(idx, self.focus.expect("focused window")); 336 | } 337 | } 338 | } 339 | 340 | pub fn move_parent_window(&mut self, op: MoveOp) -> Option { 341 | self.focus.and_then(|idx| match self.nodes.get_mut(idx) { 342 | Some(Node::Stack(s)) => { 343 | if matches!( 344 | s.focus.and_then(|idx| s.nodes.get(idx)), 345 | Some(Node::Window(_)) 346 | ) { 347 | self.focus = Some(match op { 348 | MoveOp::Down => (idx + 1) % self.nodes.len(), 349 | MoveOp::Up => (idx + self.nodes.len() - 1) % self.nodes.len(), 350 | MoveOp::Swap => 0, 351 | }); 352 | self.focused_window() 353 | } else { 354 | s.move_parent_focus(op) 355 | } 356 | } 357 | Some(Node::Window(_)) => None, 358 | None => None, 359 | }) 360 | } 361 | 362 | fn find_window(&mut self, window: Window) -> Option { 363 | self.windows_idx() 364 | .iter() 365 | .find(|(_, w)| *w == window) 366 | .map(|(i, _)| *i) 367 | } 368 | 369 | pub fn remove(&mut self, window: Window) -> bool { 370 | let res = (0..self.nodes.len()).find(|&i| match self.nodes.get_mut(i) { 371 | Some(Node::Stack(s)) => s.remove(window) && s.nodes.is_empty(), 372 | Some(Node::Window(w)) => *w == window, 373 | _ => false, 374 | }); 375 | 376 | if let Some(idx) = res { 377 | self.nodes.remove(idx); 378 | 379 | if self.nodes.is_empty() { 380 | self.focus = None; 381 | } else { 382 | self.focus = self 383 | .focus 384 | .map(|x| cmp::max(0, cmp::min(x, self.nodes.len() - 1))); 385 | } 386 | 387 | return true; 388 | } 389 | false 390 | } 391 | 392 | pub fn remove_urgent(&mut self, window: Window) -> bool { 393 | let res = self 394 | .urgent 395 | .iter() 396 | .enumerate() 397 | .find(|(_, &x)| x == window) 398 | .map(|(i, _)| i); 399 | 400 | if let Some(idx) = res { 401 | self.urgent.swap_remove(idx); 402 | true 403 | } else { 404 | self.nodes 405 | .iter_mut() 406 | .filter_map(|x| { 407 | if let Node::Stack(s) = x { 408 | Some(s.remove_urgent(window)) 409 | } else { 410 | None 411 | } 412 | }) 413 | .any(|x| x) 414 | } 415 | } 416 | 417 | pub fn send_layout_msg(&mut self, xws: &XlibWindowSystem, msg: LayoutMsg) { 418 | match self.focused_stack_mut() { 419 | Some(c) => c.send_layout_msg(xws, msg), 420 | None => if let Some(l) = self.layout.as_mut() { 421 | l.send_msg(xws, &self.nodes, msg); 422 | } 423 | } 424 | } 425 | 426 | pub fn apply_layout(&self, screen: Rect, xws: &XlibWindowSystem) -> Vec<(Rect, Window)> { 427 | if let Some(layout) = self.layout.as_ref() { 428 | layout 429 | .apply(screen, xws, self) 430 | .iter() 431 | .enumerate() 432 | .filter_map(|(idx, rect)| match self.nodes.get(idx) { 433 | Some(Node::Window(w)) => Some(vec![(*rect, *w)]), 434 | Some(Node::Stack(s)) => Some(s.apply_layout(*rect, xws)), 435 | _ => None, 436 | }) 437 | .flatten() 438 | .collect() 439 | } else { 440 | Vec::new() 441 | } 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::stack::Stack; 3 | use crate::workspace::{Workspace, WorkspaceConfig}; 4 | use crate::xlib_window_system::XlibWindowSystem; 5 | use crate::layout::Rect; 6 | use crate::ewmh; 7 | use std::cmp; 8 | use std::path::Path; 9 | use x11::xlib::Window; 10 | use anyhow::{Result, Context}; 11 | 12 | #[cfg(feature = "reload")] 13 | use serde::{Serialize, Deserialize}; 14 | 15 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 16 | pub struct WmState { 17 | workspaces: Vec, 18 | unmanaged: Vec, 19 | cur: usize, 20 | screens: Vec 21 | } 22 | 23 | impl WmState { 24 | pub fn new(ws_cfg_list: Vec, xws: &XlibWindowSystem) -> Result { 25 | let state_file_path = Path::new(&Config::get_dir()?).join(".state.tmp"); 26 | if state_file_path.exists() { 27 | 28 | #[cfg(feature = "reload")] 29 | let res = WmState::from_file(state_file_path); 30 | #[cfg(not(feature = "reload"))] 31 | let res: Result = Err(anyhow::anyhow!("missing reload support. Recompile with the reload feature enabled")); 32 | 33 | match res { 34 | Ok(state) => { 35 | state.all_ws().iter().for_each(|workspace| { 36 | workspace.all().iter().for_each(|&window| { 37 | xws.request_window_events(window); 38 | }) 39 | }); 40 | 41 | return Ok(state); 42 | }, 43 | Err(e) => error!("failed to restore previous state: {}", e) 44 | } 45 | } 46 | 47 | debug!("creating default WmState"); 48 | Ok(WmState { 49 | workspaces: ws_cfg_list 50 | .into_iter() 51 | .enumerate() 52 | .map(|(idx,c)| { 53 | Workspace { 54 | tag: c.tag.clone(), 55 | screen: c.screen, 56 | index: idx, 57 | managed: Stack::new(Some(c.layout)), 58 | focus: idx == 0, 59 | ..Default::default() 60 | } 61 | }) 62 | .collect(), 63 | unmanaged: Vec::new(), 64 | cur: 0, 65 | screens: Vec::new(), 66 | }) 67 | } 68 | 69 | #[cfg(feature = "reload")] 70 | fn from_file>(path: P) -> Result { 71 | use std::fs::{File, remove_file}; 72 | 73 | let file = File::open(&path) 74 | .context("failed to open wm state serialization file")?; 75 | 76 | debug!("loading previous wm state"); 77 | let ws: WmState = serde_json::from_reader(file) 78 | .context("failed to deserialize wm state")?; 79 | 80 | remove_file(path).ok(); 81 | 82 | Ok(ws) 83 | } 84 | 85 | pub fn get_ws(&self, index: usize) -> Option<&Workspace> { 86 | self.workspaces.get(index) 87 | } 88 | 89 | pub fn get_ws_mut(&mut self, index: usize) -> Option<&mut Workspace> { 90 | self.workspaces.get_mut(index) 91 | } 92 | 93 | pub fn current_ws(&self) -> &Workspace { 94 | self.workspaces.get(self.cur).unwrap() 95 | } 96 | 97 | pub fn current_ws_mut(&mut self) -> &mut Workspace { 98 | self.workspaces.get_mut(self.cur).unwrap() 99 | } 100 | 101 | pub fn all_ws(&self) -> &Vec { 102 | &self.workspaces 103 | } 104 | 105 | pub fn all_visible_ws(&self) -> Vec<&Workspace> { 106 | self.workspaces 107 | .iter() 108 | .filter(|ws| ws.is_visible()) 109 | .collect() 110 | } 111 | 112 | pub fn get_ws_index(&self) -> usize { 113 | self.cur 114 | } 115 | 116 | pub fn ws_count(&self) -> usize { 117 | self.workspaces.len() 118 | } 119 | 120 | pub fn get_screens(&self) -> &Vec { 121 | &self.screens 122 | } 123 | 124 | pub fn contains(&self, window: Window) -> bool { 125 | self.workspaces.iter().any(|ws| ws.contains(window)) 126 | } 127 | 128 | pub fn is_floating(&self, window: Window) -> bool { 129 | self.workspaces.iter().any(|ws| ws.is_floating(window)) 130 | } 131 | 132 | pub fn set_urgency(&mut self, is_urgent: bool, window: Window) { 133 | if let Some(ws) = self.get_parent_mut(window) { 134 | ws.set_urgency(is_urgent, window); 135 | } 136 | } 137 | 138 | pub fn add_window(&mut self, index: Option, xws: &XlibWindowSystem, config: &Config, window: Window) { 139 | if !self.contains(window) { 140 | let screens = self.screens.clone(); 141 | 142 | let parent = xws.transient_for(window) 143 | .and_then(|x| self.find_window(x)); 144 | 145 | let workspace = parent 146 | .or_else(|| index.or_else(|| Some(self.get_ws_index()))) 147 | .and_then(|idx| self.get_ws_mut(idx)) 148 | .expect("valid workspace"); 149 | 150 | workspace.add_window(xws, window); 151 | 152 | if parent.is_some() { 153 | workspace.focus_window(xws, window); 154 | } 155 | 156 | if !workspace.is_visible() { 157 | workspace.set_urgency(true, window); 158 | } else { 159 | //workspace.focus_window(xws, window); 160 | workspace.redraw(xws, config, &screens); 161 | xws.show_window(window); 162 | self.raise_sticky(xws); 163 | } 164 | 165 | ewmh::set_client_list(xws, &self.workspaces); 166 | } 167 | } 168 | 169 | pub fn focus_window(&mut self, xws: &XlibWindowSystem, config: &Config, window: Window, force_switch: bool) { 170 | if xws.get_wm_hints(window).map(|x| x.input != 0).unwrap_or(true) { 171 | if let Some(index) = self.find_window(window) { 172 | if self.cur != index { 173 | let workspace = self.get_ws_mut(index) 174 | .expect("valid workspace"); 175 | 176 | workspace.focus_window(xws, window); 177 | 178 | if force_switch || workspace.is_visible() { 179 | self.switch_to_ws(xws, config, index, false); 180 | } 181 | } else { 182 | self.current_ws_mut().focus_window(xws, window); 183 | } 184 | } 185 | } 186 | } 187 | 188 | pub fn switch_to_ws(&mut self, xws: &XlibWindowSystem, config: &Config, index: usize, center_pointer: bool) { 189 | if self.cur == index || index >= self.workspaces.len() { 190 | return 191 | } 192 | 193 | // implies that the target workspace is on another screen 194 | if self.workspaces[index].visible { 195 | if config.greedy_view { 196 | self.switch_screens(xws, index); 197 | self.workspaces[self.cur].redraw(xws, config, &self.screens); 198 | self.workspaces[self.cur].show(xws); 199 | } else if center_pointer { 200 | self.workspaces[index].center_pointer(xws); 201 | } 202 | } else { 203 | self.workspaces[index].screen = self.workspaces[self.cur].screen; 204 | self.workspaces[index].redraw(xws, config, &self.screens); 205 | self.workspaces[index].show(xws); 206 | self.workspaces[self.cur].hide(xws); 207 | } 208 | 209 | self.workspaces[self.cur].unfocus(xws, config); 210 | self.workspaces[index].focus(xws); 211 | 212 | self.cur = index; 213 | 214 | self.raise_sticky(xws); 215 | ewmh::set_current_desktop(xws, index); 216 | ewmh::set_desktop_viewport(xws, self.all_ws()); 217 | } 218 | 219 | pub fn switch_to_ws_at(&mut self, xws: &XlibWindowSystem, config: &Config, x: u32, y: u32, center_pointer: bool) { 220 | let ws_idx = self.workspaces 221 | .iter() 222 | .enumerate() 223 | .find(|(_,ws)| { 224 | let screen = self.screens[ws.get_screen()]; 225 | x >= screen.x && x < screen.x + screen.width && 226 | y >= screen.y && y < screen.y + screen.height && 227 | ws.is_visible() 228 | }) 229 | .map(|(idx,_)| idx); 230 | 231 | if let Some(idx) = ws_idx { 232 | self.switch_to_ws(xws, config, idx, center_pointer); 233 | } 234 | } 235 | 236 | pub fn switch_to_screen(&mut self, xws: &XlibWindowSystem, config: &Config, screen: usize) { 237 | if screen > self.screens.len() - 1 { 238 | return; 239 | } 240 | 241 | let idx_workspace = self.workspaces.iter() 242 | .enumerate() 243 | .filter(|&(i, workspace)| workspace.screen == screen && workspace.visible && i != self.cur) 244 | .map(|(i, _)| i) 245 | .last(); 246 | 247 | if let Some(index) = idx_workspace { 248 | self.workspaces[self.cur].unfocus(xws, config); 249 | self.workspaces[index].focus(xws); 250 | 251 | self.workspaces[index].center_pointer(xws); 252 | self.cur = index; 253 | 254 | ewmh::set_current_desktop(xws, index); 255 | } 256 | } 257 | 258 | pub fn move_window_to_ws(&mut self, xws: &XlibWindowSystem, config: &Config, index: usize) { 259 | if index == self.cur || index >= self.ws_count() { 260 | return; 261 | } 262 | 263 | if let Some(window) = self.current_ws().focused_window() { 264 | self.remove_window(xws, config, window); 265 | 266 | let ws = &mut self.workspaces[index]; 267 | ws.add_window(xws, window); 268 | ws.focus_window(xws, window); 269 | ws.redraw(xws, config, &self.screens); 270 | self.raise_sticky(xws); 271 | 272 | ewmh::set_wm_desktop(xws, window, index); 273 | if self.current_ws().is_empty() { 274 | ewmh::set_active_window(xws, 0); 275 | } 276 | } 277 | 278 | } 279 | 280 | pub fn move_window_to_screen(&mut self, xws: &XlibWindowSystem, config: &Config, screen: usize) { 281 | if let Some((index,_)) = self.all_ws() 282 | .iter() 283 | .enumerate() 284 | .find(|&(_, ws)| ws.is_visible() && ws.screen == screen) 285 | { 286 | self.move_window_to_ws(xws, config, index); 287 | } 288 | } 289 | 290 | pub fn remove_window(&mut self, xws: &XlibWindowSystem, config: &Config, window: Window) { 291 | let screens = self.screens.clone(); 292 | 293 | if let Some(workspace) = self.get_parent_mut(window) { 294 | workspace.remove_window(xws, window); 295 | 296 | if workspace.is_visible() { 297 | if let Some(w) = workspace.focused_window() { 298 | xws.focus_window(w); 299 | } 300 | workspace.redraw(xws, config, &screens); 301 | } 302 | 303 | if workspace.is_empty() { 304 | ewmh::set_active_window(xws, 0); 305 | } 306 | 307 | self.raise_sticky(xws); 308 | ewmh::set_client_list(xws, &self.workspaces); 309 | } 310 | } 311 | /* 312 | pub fn hide_window(&mut self, window: Window) { 313 | if let Some(idx) = self.find_window(window) { 314 | self.list.get_mut(idx) 315 | .expect("valid workspace") 316 | .hide_window(window); 317 | } 318 | } 319 | */ 320 | pub fn rescreen(&mut self, xws: &XlibWindowSystem, config: &Config) { 321 | let n_old_screens = self.screens.len(); 322 | 323 | let screens = xws.get_screen_infos(); 324 | trace!("xinerama screens: {:?}", screens); 325 | 326 | let mut new_screens: Vec = Vec::new(); 327 | 328 | for screen in screens { 329 | if let Some(old_screen) = new_screens.iter_mut() 330 | .find(|s| s.x == screen.x && s.y == screen.y) 331 | { 332 | old_screen.width = cmp::min(old_screen.width, screen.width); 333 | old_screen.height = cmp::min(old_screen.height, screen.height) 334 | } else { 335 | new_screens.push(screen); 336 | } 337 | } 338 | 339 | let n_new_screens = new_screens.len(); 340 | 341 | // move and hide workspaces if their screens got removed 342 | if n_new_screens < n_old_screens { 343 | for workspace in self.workspaces.iter_mut().filter(|x| x.screen > n_new_screens - 1) { 344 | workspace.screen = 0; 345 | workspace.hide(xws); 346 | } 347 | } else { 348 | trace!("finding workspace to rescreen"); 349 | // assign the first hidden workspace to the new screen 350 | for screen in n_old_screens..n_new_screens { 351 | trace!("rescreen for screen {}", screen); 352 | if let Some(workspace) = self.workspaces.iter_mut().find(|ws| !ws.is_visible()) { 353 | trace!("moving workspace {}", workspace.tag); 354 | workspace.screen = screen; 355 | workspace.show(xws); 356 | } 357 | } 358 | } 359 | 360 | self.screens = new_screens; 361 | debug!("rescreen: {:?}", self.screens); 362 | self.redraw(xws, config); 363 | 364 | ewmh::set_desktop_viewport(xws, self.all_ws()); 365 | } 366 | 367 | pub fn find_window(&self, window: Window) -> Option { 368 | self.workspaces.iter().enumerate().find(|(_, workspace)| workspace.contains(window)).map(|(idx, _)| idx) 369 | } 370 | 371 | pub fn get_parent_mut(&mut self, window: Window) -> Option<&mut Workspace> { 372 | self.workspaces.iter_mut().find(|workspace| workspace.contains(window)) 373 | } 374 | 375 | fn switch_screens(&mut self, xws: &XlibWindowSystem, dest: usize) { 376 | let screen = self.current_ws().get_screen(); 377 | self.workspaces[self.cur].screen = self.workspaces[dest].screen; 378 | self.workspaces[dest].screen = screen; 379 | 380 | ewmh::set_desktop_viewport(xws, self.all_ws()); 381 | } 382 | 383 | pub fn raise_sticky(&self, xws: &XlibWindowSystem) { 384 | for w in &self.unmanaged { 385 | xws.raise_window(*w); 386 | } 387 | } 388 | 389 | pub fn redraw(&self, xws: &XlibWindowSystem, config: &Config) { 390 | for ws in self.all_visible_ws() { 391 | ws.redraw(xws, config, &self.screens); 392 | } 393 | self.raise_sticky(xws); 394 | } 395 | 396 | pub fn redraw_current(&self, xws: &XlibWindowSystem, config: &Config) { 397 | self.current_ws().redraw(xws, config, self.get_screens()); 398 | self.raise_sticky(xws); 399 | } 400 | 401 | pub fn add_unmanaged(&mut self, window: Window) { 402 | println!("add sticky: {}", window); 403 | if !self.unmanaged.contains(&window) { 404 | self.unmanaged.push(window); 405 | } 406 | } 407 | 408 | pub fn try_remove_unmanaged(&mut self, window: Window) -> bool { 409 | if let Some((idx,_)) = self.unmanaged.iter() 410 | .enumerate() 411 | .find(|(_,&x)| x == window) 412 | { 413 | self.unmanaged.swap_remove(idx); 414 | true 415 | } else { 416 | false 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/statusbar.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use crate::state::WmState; 4 | use crate::xlib_window_system::XlibWindowSystem; 5 | use crate::config::{PagerInfo, WorkspaceInfo}; 6 | use std::io::Write; 7 | use std::process::{Command, Child, Stdio}; 8 | use anyhow::{anyhow, bail, Context, Result}; 9 | 10 | pub struct Statusbar { 11 | executable: String, 12 | args: Option>, 13 | fn_format: Box String>, 14 | } 15 | 16 | impl Statusbar { 17 | pub fn new(executable: String, 18 | args: Option>, 19 | fn_format: Box String>) 20 | -> Statusbar { 21 | Statusbar { 22 | executable, 23 | args, 24 | fn_format, 25 | } 26 | } 27 | 28 | pub fn xmobar() -> Statusbar { 29 | Statusbar::new("xmobar".to_string(), 30 | None, 31 | Box::new(move |info: PagerInfo| -> String { 32 | let workspaces = info.workspaces 33 | .iter() 34 | .map(|x| { 35 | let (fg, bg) = if x.current { 36 | ("#00ff00", "#000000") 37 | } else if x.visible { 38 | ("#009900", "#000000") 39 | } else if x.urgent { 40 | ("#ff0000", "#000000") 41 | } else { 42 | ("#ffffff", "#000000") 43 | }; 44 | format!("[{}]", fg, bg, x.tag) 45 | }) 46 | .collect::>() 47 | .join(" "); 48 | 49 | format!("{} | {} | {}\n", 50 | workspaces, 51 | info.layout_names.join("/"), 52 | info.window_title) 53 | })) 54 | } 55 | 56 | pub fn start(&self) -> Result { 57 | debug!("starting statusbar {}", self.executable); 58 | let mut cmd = Command::new(self.executable.clone()); 59 | 60 | if self.args.is_some() { 61 | cmd.args(self.args.clone().expect("args missing").as_slice()); 62 | } 63 | 64 | cmd.stdin(Stdio::piped()).spawn() 65 | .context(format!("failed to execute '{}'", self.executable)) 66 | } 67 | 68 | pub fn update(&self, child: &mut Child, xws: &XlibWindowSystem, state: &WmState) -> Result<()> { 69 | let layout_names = state 70 | .current_ws() 71 | .managed 72 | .layout_iter() 73 | .map(|x| x.name()) 74 | .collect(); 75 | 76 | let output = (self.fn_format)(PagerInfo { 77 | workspaces: state.all_ws() 78 | .iter() 79 | .enumerate() 80 | .map(|(i, x)| { 81 | WorkspaceInfo { 82 | id: i, 83 | tag: x.get_tag().to_string(), 84 | screen: x.get_screen(), 85 | current: i == state.get_ws_index(), 86 | visible: x.is_visible(), 87 | urgent: x.is_urgent(), 88 | } 89 | }) 90 | .collect(), 91 | layout_names, 92 | window_title: xws.get_window_title(state.current_ws().focused_window().unwrap_or(0)), 93 | }); 94 | 95 | let stdin = child 96 | .stdin 97 | .as_mut() 98 | .ok_or_else(|| anyhow!("failed to get statusbar stdin"))?; 99 | 100 | stdin.write_all(output.as_bytes()) 101 | .context("failed to write to statusbar stdin")?; 102 | 103 | Ok(()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, Stdio}; 2 | use std::env; 3 | use anyhow::{Error, Result}; 4 | 5 | #[allow(dead_code)] 6 | pub fn xmessage(msg: &str) -> Result<()> { 7 | Command::new("xmessage") 8 | .arg("-center") 9 | .arg(msg) 10 | .output() 11 | .map_err(|e| e.into()) 12 | .map(|_| ()) 13 | } 14 | 15 | #[allow(dead_code)] 16 | pub fn concat_error_chain(err: &Error) -> String { 17 | let mut msgs = Vec::new(); 18 | 19 | msgs.push(format!("ERROR: {err}")); 20 | 21 | err.chain().skip(1).for_each(|cause| msgs.push(format!("because: {cause}"))); 22 | 23 | msgs.as_slice().join("\n") 24 | } 25 | 26 | pub fn exec(cmd: String, args: Vec) { 27 | if !cmd.is_empty() { 28 | std::thread::spawn(move || { 29 | match Command::new(&cmd) 30 | .envs(env::vars()) 31 | .stdin(Stdio::null()) 32 | .stdout(Stdio::null()) 33 | .stderr(Stdio::null()) 34 | .args(&args) 35 | .spawn() 36 | { 37 | Ok(mut child) => { 38 | child.wait().ok(); 39 | }, 40 | Err(e) => error!("failed to start \"{:?}\": {}", cmd, e), 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/workspace.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::ewmh; 3 | use crate::layout::{Layout, Tall}; 4 | use crate::layout::{LayoutMsg, Rect}; 5 | use crate::stack::Stack; 6 | use crate::xlib_window_system::XlibWindowSystem; 7 | use std::cmp; 8 | use x11::xlib::Window; 9 | 10 | #[cfg(feature = "reload")] 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[allow(dead_code)] 14 | pub struct WorkspaceInfo { 15 | pub tags: String, 16 | pub layout: String, 17 | pub current: bool, 18 | pub visible: bool, 19 | pub urgent: bool, 20 | } 21 | 22 | pub struct WorkspaceConfig { 23 | pub tag: String, 24 | pub screen: usize, 25 | pub layout: Box, 26 | } 27 | 28 | pub enum MoveOp { 29 | Up, 30 | Down, 31 | Swap, 32 | } 33 | 34 | #[cfg_attr(feature = "reload", derive(Serialize, Deserialize))] 35 | pub struct Workspace { 36 | pub(crate) managed: Stack, 37 | pub(crate) floating: Stack, 38 | pub(crate) tag: String, 39 | pub index: usize, 40 | pub screen: usize, 41 | pub visible: bool, 42 | pub focus: bool, 43 | } 44 | 45 | impl Default for Workspace { 46 | fn default() -> Self { 47 | Self { 48 | managed: Stack::new(Some(Tall::new(1, 0.5, 0.05))), 49 | floating: Stack::new(None), 50 | tag: String::new(), 51 | index: 0, 52 | screen: 0, 53 | visible: false, 54 | focus: false, 55 | } 56 | } 57 | } 58 | 59 | impl Workspace { 60 | pub fn all(&self) -> Vec { 61 | self.floating 62 | .all_windows() 63 | .iter() 64 | .chain(self.managed.all_windows().iter()) 65 | .copied() 66 | .collect() 67 | } 68 | 69 | fn all_urgent(&self) -> Vec<&Window> { 70 | self.floating 71 | .urgent 72 | .iter() 73 | .chain(self.managed.urgent.iter()) 74 | .collect() 75 | } 76 | 77 | pub fn is_empty(&self) -> bool { 78 | (self.floating.len() + self.managed.len()) == 0 79 | } 80 | 81 | pub fn send_layout_message(&mut self, xws: &XlibWindowSystem, msg: LayoutMsg) { 82 | self.managed.send_layout_msg(xws, msg); 83 | } 84 | 85 | pub fn get_tag(&self) -> &str { 86 | &self.tag 87 | } 88 | 89 | pub fn get_screen(&self) -> usize { 90 | self.screen 91 | } 92 | 93 | pub fn is_floating(&self, window: Window) -> bool { 94 | self.floating.contains(window) 95 | } 96 | 97 | pub fn is_managed(&self, window: Window) -> bool { 98 | self.managed.contains(window) 99 | } 100 | 101 | pub fn is_urgent(&self) -> bool { 102 | self.managed.is_urgent() || self.floating.is_urgent() 103 | } 104 | 105 | pub fn is_visible(&self) -> bool { 106 | self.visible 107 | } 108 | 109 | pub fn focused_window(&self) -> Option { 110 | self.floating 111 | .focused_window() 112 | .or_else(|| self.managed.focused_window()) 113 | } 114 | 115 | pub fn add_window(&mut self, xws: &XlibWindowSystem, window: Window) { 116 | if !xws.is_floating_window(window) { 117 | debug!("Add Managed: {:#x}", window); 118 | self.managed.add_window(window); 119 | 120 | if self.floating.len() > 0 { 121 | debug!("Restacking"); 122 | xws.restack_windows(self.all()); 123 | } 124 | } else { 125 | self.floating.add_window(window); 126 | debug!("Add Unmanaged: {:#x}", window); 127 | } 128 | } 129 | 130 | pub fn nest_layout(&mut self, layout: Box) { 131 | if self.managed.len() > 1 { 132 | self.managed.add_container(layout); 133 | } else if let Some(s) = self.managed.all_stacks_mut().first_mut() { 134 | if s.len() < 1 { 135 | s.layout = Some(layout); 136 | } else { 137 | self.managed.add_container(layout); 138 | } 139 | } else { 140 | self.managed.add_container(layout); 141 | } 142 | } 143 | 144 | pub fn set_urgency(&mut self, urgent: bool, window: Window) { 145 | trace!("urgency {:#x} {}", window, urgent); 146 | 147 | if !urgent { 148 | if self.is_urgent() { 149 | self.remove_urgent_window(window); 150 | } 151 | } else if self.is_managed(window) { 152 | self.managed.urgent.push(window); 153 | } else { 154 | self.floating.urgent.push(window); 155 | } 156 | } 157 | 158 | fn remove_urgent_window(&mut self, window: Window) { 159 | if !self.managed.remove_urgent(window) { 160 | self.floating.remove_urgent(window); 161 | } 162 | } 163 | 164 | fn remove_managed(&mut self, xws: &XlibWindowSystem, window: Window) { 165 | self.managed.remove(window); 166 | xws.unmap_window(window); 167 | } 168 | 169 | fn remove_floating(&mut self, xws: &XlibWindowSystem, window: Window) { 170 | if !ewmh::is_window_sticky(xws, window) { 171 | xws.unmap_window(window); 172 | } 173 | 174 | self.floating.remove(window); 175 | self.floating.focus = if self.floating.nodes.is_empty() { 176 | None 177 | } else { 178 | Some(self.floating.nodes.len() - 1) 179 | }; 180 | } 181 | 182 | pub fn remove_window(&mut self, xws: &XlibWindowSystem, window: Window) -> bool { 183 | if self.managed.contains(window) { 184 | trace!("Remove Managed: {:#x}", window); 185 | self.remove_managed(xws, window); 186 | } else if self.floating.contains(window) { 187 | trace!("Remove Unmanaged: {:#x}", window); 188 | self.remove_floating(xws, window); 189 | } else { 190 | return false; 191 | } 192 | 193 | true 194 | } 195 | 196 | pub fn focus_window(&mut self, xws: &XlibWindowSystem, window: Window) -> bool { 197 | if window == 0 || self.managed.focused_window() == Some(window) { 198 | return false; 199 | } 200 | 201 | if self.is_visible() { 202 | self.remove_urgent_window(window); 203 | } 204 | 205 | if self.floating.contains(window) { 206 | self.floating.focus_window(window); 207 | } else { 208 | self.managed.focus_window(window); 209 | } 210 | 211 | xws.focus_window(window); 212 | true 213 | } 214 | 215 | pub fn move_parent_focus(&mut self, op: MoveOp) -> Option { 216 | let prev_focus = self.focused_window(); 217 | let new_focus = self.managed.move_parent_focus(op); 218 | 219 | if new_focus != prev_focus { 220 | new_focus 221 | } else { 222 | None 223 | } 224 | } 225 | 226 | // TODO: impl managed and floating focus mode incl switching 227 | pub fn move_focus(&mut self, op: MoveOp) -> Option { 228 | let prev_focus = self.focused_window(); 229 | let new_focus = self.managed.move_focus(op); 230 | 231 | if new_focus != prev_focus { 232 | new_focus 233 | } else { 234 | None 235 | } 236 | } 237 | 238 | pub fn move_window(&mut self, op: MoveOp) -> bool { 239 | if let Some(window) = self.focused_window() { 240 | trace!("move window: {:?}", window); 241 | 242 | self.managed.move_window(op); 243 | true 244 | } else { 245 | false 246 | } 247 | } 248 | 249 | pub fn move_parent_window(&mut self, op: MoveOp) -> bool { 250 | if let Some(window) = self.focused_window() { 251 | trace!("move parent window: {:?}", window); 252 | 253 | self.managed.move_parent_window(op); 254 | true 255 | } else { 256 | false 257 | } 258 | } 259 | 260 | pub fn contains(&self, window: Window) -> bool { 261 | self.all().iter().any(|&w| w == window) 262 | } 263 | 264 | pub fn unfocus(&mut self, xws: &XlibWindowSystem, config: &Config) { 265 | self.focus = false; 266 | 267 | if let Some(window) = self.focused_window() { 268 | trace!("unfocus window: {:#x}", window); 269 | xws.set_window_border_color(window, config.border_color); 270 | } 271 | } 272 | 273 | pub fn focus(&mut self, xws: &XlibWindowSystem) { 274 | self.focus = true; 275 | 276 | if let Some(window) = self 277 | .focused_window() 278 | .or_else(|| self.all().first().copied()) 279 | { 280 | xws.focus_window(window); 281 | } else { 282 | xws.focus_window(xws.get_root_window()); 283 | } 284 | } 285 | 286 | pub fn center_pointer(&self, xws: &XlibWindowSystem) { 287 | let rect = if let Some(window) = self.focused_window() { 288 | xws.get_geometry(window) 289 | } else { 290 | xws.get_screen_infos()[self.screen] 291 | }; 292 | 293 | xws.move_pointer( 294 | (rect.x + (rect.width / 2)) as i32 - 1, 295 | (rect.y + (rect.height / 2)) as i32, 296 | ); 297 | } 298 | 299 | pub fn hide(&mut self, xws: &XlibWindowSystem) { 300 | self.visible = false; 301 | 302 | for &w in self 303 | .managed 304 | .all_windows() 305 | .iter() 306 | .filter(|&w| Some(*w) != self.focused_window()) 307 | { 308 | xws.hide_window(w); 309 | } 310 | 311 | for &w in self 312 | .floating 313 | .all_windows() 314 | .iter() 315 | .filter(|&w| Some(*w) != self.focused_window()) 316 | { 317 | xws.hide_window(w); 318 | } 319 | 320 | if let Some(w) = self.focused_window() { 321 | xws.hide_window(w); 322 | } 323 | } 324 | 325 | pub fn show(&mut self, xws: &XlibWindowSystem) { 326 | self.visible = true; 327 | 328 | for &w in self.managed.all_windows().iter() { 329 | xws.show_window(w); 330 | ewmh::set_wm_desktop(xws, w, self.index); 331 | } 332 | 333 | for &w in self.floating.all_windows().iter() { 334 | xws.show_window(w); 335 | ewmh::set_wm_desktop(xws, w, self.index); 336 | } 337 | } 338 | 339 | pub fn redraw(&self, xws: &XlibWindowSystem, config: &Config, screens: &[Rect]) { 340 | trace!("Redraw workspace: {}", self.tag); 341 | let screen = screens[self.screen]; 342 | let curr_focus = self.focused_window(); 343 | 344 | for (rect, window) in self.managed.apply_layout(screen, xws) { 345 | let is_fullscreen = ewmh::is_window_fullscreen(xws, window); 346 | let border_color = if Some(window) == curr_focus { 347 | config.border_focus_color 348 | } else { 349 | config.border_color 350 | }; 351 | 352 | if is_fullscreen { 353 | xws.raise_window(window); 354 | xws.setup_window( 355 | screen.x, 356 | screen.y, 357 | screen.width, 358 | screen.height, 359 | 0, 360 | border_color, 361 | window, 362 | ); 363 | } else { 364 | xws.setup_window( 365 | rect.x, 366 | rect.y, 367 | rect.width, 368 | rect.height, 369 | config.border_width, 370 | border_color, 371 | window, 372 | ); 373 | } 374 | } 375 | 376 | for &window in self.floating.all_windows().iter() { 377 | let mut rect = xws.get_geometry(window); 378 | rect.width = cmp::min(screen.width, rect.width + (2 * config.border_width)); 379 | rect.height = cmp::min(screen.height, rect.height + (2 * config.border_width)); 380 | let border_color = if Some(window) == curr_focus { 381 | config.border_focus_color 382 | } else { 383 | config.border_color 384 | }; 385 | 386 | xws.raise_window(window); 387 | xws.setup_window( 388 | screen.x + (screen.width - rect.width) / 2, 389 | screen.y + (screen.height - rect.height) / 2, 390 | rect.width, 391 | rect.height, 392 | config.border_width, 393 | border_color, 394 | window, 395 | ); 396 | } 397 | 398 | for &window in self.all_urgent() { 399 | xws.set_window_border_color(window, config.border_urgent_color); 400 | } 401 | 402 | xws.skip_enter_events(); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/xlib_window_system.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals, unused_variables, dead_code)] 2 | #![allow(clippy::too_many_arguments)] 3 | 4 | extern crate libc; 5 | 6 | use crate::keycode::{MOD_2, MOD_LOCK}; 7 | use crate::layout::Rect; 8 | use crate::ewmh; 9 | use std::{cmp, env, ptr, str}; 10 | use std::mem::MaybeUninit; 11 | use std::slice::from_raw_parts; 12 | use std::ffi::{CStr, CString}; 13 | use std::time::{SystemTime, UNIX_EPOCH}; 14 | use std::collections::HashMap; 15 | use self::libc::{c_void, c_char, c_uchar, c_int, c_uint, c_long, c_ulong}; 16 | use self::XlibEvent::*; 17 | use x11::xinerama::XineramaQueryScreens; 18 | use x11::xlib::*; 19 | 20 | extern "C" fn error_handler(display: *mut Display, event: *mut XErrorEvent) -> c_int { 21 | // TODO: proper error handling 22 | // HACK: fixes LeaveNotify on invalid windows 23 | 0 24 | } 25 | 26 | pub enum XlibEvent { 27 | XMapRequest(Window, bool), 28 | XClientMessage(Window, Atom, ClientMessageData), 29 | XConfigureNotify(Window), 30 | XConfigureRequest(Window, WindowChanges, u32), 31 | XDestroy(Window), 32 | XUnmapNotify(Window, bool), 33 | XPropertyNotify(Window, u64, bool), 34 | XEnterNotify(Window, bool, u32, u32), 35 | XFocusIn(Window), 36 | XFocusOut(Window), 37 | XKeyPress(Window, u8, String), 38 | XButtonPress(Window), 39 | WMClose, 40 | Ignored, 41 | } 42 | 43 | pub struct SizeHint { 44 | pub min: Option<(u32, u32)>, 45 | pub max: Option<(u32, u32)>, 46 | } 47 | 48 | pub struct Strut(pub u32, pub u32, pub u32, pub u32); 49 | 50 | pub struct WindowChanges { 51 | pub x: u32, 52 | pub y: u32, 53 | pub width: u32, 54 | pub height: u32, 55 | pub border_width: u32, 56 | pub sibling: Window, 57 | pub stack_mode: u32, 58 | } 59 | 60 | pub struct XlibWindowSystem { 61 | display: *mut Display, 62 | root: Window, 63 | event: *mut c_void, 64 | atoms: HashMap<&'static str, Atom>, 65 | } 66 | 67 | impl XlibWindowSystem { 68 | pub fn new() -> XlibWindowSystem { 69 | unsafe { 70 | let display = XOpenDisplay(ptr::null_mut()); 71 | if display.is_null() { 72 | error!("Can't open display {}", 73 | env::var("DISPLAY").unwrap_or_else(|_| "undefined".to_string())); 74 | panic!(); 75 | } 76 | 77 | let root = XDefaultRootWindow(display); 78 | 79 | XlibWindowSystem { 80 | display, 81 | root, 82 | event: libc::malloc(256), 83 | atoms: HashMap::new(), 84 | } 85 | } 86 | } 87 | 88 | pub fn init(&mut self) { 89 | unsafe { 90 | XSelectInput(self.display, self.root, 0x001A_0034 | EnterWindowMask | PropertyChangeMask); 91 | XDefineCursor(self.display, self.root, XCreateFontCursor(self.display, 68)); 92 | XSetErrorHandler(Some(error_handler)); 93 | XSync(self.display, 0); 94 | } 95 | 96 | self.cache_atoms(&["WM_DELETE_WINDOW", "WM_HINTS", "WM_PROTOCOLS", "WM_STATE", "WM_TAKE_FOCUS", "UTF8_STRING"]); 97 | ewmh::init_ewmh(self); 98 | } 99 | 100 | pub fn close(&mut self) { 101 | unsafe { 102 | XCloseDisplay(self.display); 103 | self.display = ptr::null_mut(); 104 | libc::free(self.event); 105 | } 106 | } 107 | 108 | pub fn get_root_window(&self) -> Window { 109 | self.root 110 | } 111 | 112 | pub fn setup_window(&self, 113 | x: u32, 114 | y: u32, 115 | width: u32, 116 | height: u32, 117 | border_width: u32, 118 | border_color: u32, 119 | window: Window) { 120 | self.set_window_border_width(window, border_width); 121 | self.set_window_border_color(window, border_color); 122 | 123 | self.move_resize_window(window, 124 | x, 125 | y, 126 | cmp::max(width as i32 - (2 * border_width as i32), 0) as u32, 127 | cmp::max(height as i32 - (2 * border_width as i32), 0) as u32); 128 | } 129 | 130 | pub fn create_hidden_window(&self) -> Window { 131 | unsafe { 132 | XCreateSimpleWindow(self.display, self.root, -1, -1, 1, 1, 0, 0, 0) 133 | } 134 | } 135 | 136 | pub fn get_property(&self, window: Window, atom: A) -> Option> { 137 | unsafe { 138 | let mut ret_type: c_ulong = 0; 139 | let mut ret_format: c_int = 0; 140 | let mut ret_nitems: c_ulong = 0; 141 | let mut ret_bytes_after: c_ulong = 0; 142 | let mut ret_ptr = MaybeUninit::<*mut c_ulong>::uninit(); 143 | 144 | if XGetWindowProperty(self.display, 145 | window, 146 | atom.into(self), 147 | 0, 148 | 0xFFFF_FFFF, 149 | 0, 150 | 0, 151 | &mut ret_type, 152 | &mut ret_format, 153 | &mut ret_nitems, 154 | &mut ret_bytes_after, 155 | ret_ptr.as_mut_ptr() as *mut *mut c_uchar) == 0 { 156 | if ret_format != 0 { 157 | let ret_ptr = ret_ptr.assume_init(); 158 | let vec = from_raw_parts(ret_ptr as *const c_ulong, ret_nitems as usize).to_vec(); 159 | XFree(ret_ptr as *mut c_void); 160 | Some(vec) 161 | } else { 162 | None 163 | } 164 | } else { 165 | None 166 | } 167 | } 168 | } 169 | 170 | pub fn cache_atoms(&mut self, atoms_str: &[&'static str]) { 171 | let atoms = self.create_atoms(atoms_str); 172 | 173 | for (s,a) in atoms_str.iter().zip(atoms.iter()) { 174 | self.atoms.insert(s, *a); 175 | } 176 | } 177 | 178 | fn create_atom(&self, atom: &str) -> u64 { 179 | unsafe { 180 | let cstr = CString::new(atom.as_bytes()) 181 | .unwrap(); 182 | XInternAtom(self.display, cstr.as_ptr() as *mut i8, 0) 183 | } 184 | } 185 | 186 | fn create_atoms(&self, atoms: &[&'static str]) -> Vec { 187 | let mut atoms: Vec<*mut c_char> = atoms.iter() 188 | .map(|x| { 189 | CString::new(x.as_bytes()) 190 | .unwrap() 191 | .into_raw() 192 | }) 193 | .collect(); 194 | 195 | let mut ret_atoms: Vec = Vec::with_capacity(atoms.len()); 196 | unsafe { 197 | XInternAtoms(self.display, atoms.as_mut_ptr(), atoms.len() as i32, 0, ret_atoms.as_ptr() as *mut u64); 198 | ret_atoms.set_len(atoms.len()); 199 | } 200 | 201 | for x in atoms { 202 | unsafe { 203 | drop(CString::from_raw(x)); 204 | } 205 | } 206 | 207 | ret_atoms 208 | } 209 | 210 | pub fn get_atom(&self, atom: &str) -> u64 { 211 | self.atoms.get(atom) 212 | .cloned() 213 | .unwrap_or_else(|| { trace!("cache miss for: {}", atom); self.create_atom(atom) }) 214 | } 215 | 216 | pub fn get_atom_name(&self, atom: u64) -> String { 217 | unsafe { 218 | let ptr = XGetAtomName(self.display, atom); 219 | let ret = Self::ptr_to_string(ptr); 220 | XFree(ptr as *mut c_void); 221 | ret 222 | } 223 | } 224 | 225 | pub fn get_windows(&self) -> Vec { 226 | unsafe { 227 | let mut ret_root: c_ulong = 0; 228 | let mut ret_parent: c_ulong = 0; 229 | let mut ret_nchildren: c_uint = 0; 230 | let mut ret_ptr = MaybeUninit::<*mut c_ulong>::uninit(); 231 | 232 | XQueryTree(self.display, 233 | self.root, 234 | &mut ret_root, 235 | &mut ret_parent, 236 | ret_ptr.as_mut_ptr(), 237 | &mut ret_nchildren); 238 | 239 | let ret_ptr = ret_ptr.assume_init(); 240 | let vec = from_raw_parts(ret_ptr, ret_nchildren as usize).to_vec(); 241 | XFree(ret_ptr as *mut c_void); 242 | vec 243 | } 244 | } 245 | 246 | pub fn get_window_strut(&self, window: Window) -> Option> { 247 | let atom = self.get_atom("_NET_WM_STRUT_PARTIAL"); 248 | self.get_property(window, atom) 249 | } 250 | 251 | pub fn get_window_state(&self, window: Window) -> Option { 252 | let atom = self.get_atom("WM_STATE"); 253 | self.get_property(window, atom) 254 | .and_then(|x| x.first().map(|s| *s as u8)) 255 | } 256 | 257 | pub fn get_window_attributes(&self, window: Window) -> XWindowAttributes { 258 | unsafe { 259 | let mut ret_attributes = MaybeUninit::::zeroed(); 260 | XGetWindowAttributes(self.display, window, ret_attributes.as_mut_ptr()); 261 | 262 | ret_attributes.assume_init() 263 | } 264 | } 265 | 266 | // TODO: cache result and split into computation and getter functions. 267 | // Struts rarely change and dont have to be computed on every redraw (see strut layout) 268 | pub fn compute_struts(&self, screen: Rect) -> Strut { 269 | self.get_windows() 270 | .iter() 271 | .filter_map(|&w| { 272 | self.get_window_strut(w) 273 | }) 274 | .filter(|x| { 275 | let screen_x = u64::from(screen.x); 276 | let screen_y = u64::from(screen.y); 277 | let screen_height = u64::from(screen.height); 278 | let screen_width = u64::from(screen.width); 279 | 280 | (x[0] > 0 && 281 | ((x[4] >= screen_y && x[4] < screen_y + screen_height) || 282 | (x[5] >= screen_y && x[5] <= screen_y + screen_height))) || 283 | (x[1] > 0 && 284 | ((x[6] >= screen_y && x[6] < screen_y + screen_height) || 285 | (x[7] >= screen_y && x[7] <= screen_y + screen_height))) || 286 | (x[2] > 0 && 287 | ((x[8] >= screen_x && x[8] < screen_x + screen_width) || 288 | (x[9] >= screen_x && x[9] <= screen_x + screen_width))) || 289 | (x[3] > 0 && 290 | ((x[10] >= screen_x && x[10] < screen_x + screen_width) || 291 | (x[11] >= screen_x && x[11] <= screen_x + screen_width))) 292 | }) 293 | .map(|x| Strut(x[0] as u32, x[1] as u32, x[2] as u32, x[3] as u32)) 294 | .fold(Strut(0, 0, 0, 0), |a, b| { 295 | Strut(cmp::max(a.0, b.0), 296 | cmp::max(a.1, b.1), 297 | cmp::max(a.2, b.2), 298 | cmp::max(a.3, b.3)) 299 | }) 300 | } 301 | 302 | pub fn change_property, A: IntoAtom>(&self, 303 | window: Window, 304 | atom: &str, 305 | atom_type: A, 306 | mode: c_int, 307 | data: &[T]) 308 | { 309 | unsafe { 310 | XChangeProperty(self.display, 311 | window, 312 | self.get_atom(atom), 313 | atom_type.into(self), 314 | // Xlib requires char for format 8, short for 16 and 32 for long 315 | // skipping over int32 for some reason 316 | std::cmp::min(32, std::mem::size_of::() as i32 * 8), 317 | mode, 318 | data.as_ptr().cast::(), 319 | data.len() as i32); 320 | } 321 | } 322 | 323 | pub fn send_client_message>(&self, window: Window, atom: &str, data: T) { 324 | let mut event = XClientMessageEvent { 325 | type_: ClientMessage, 326 | serial: 0, 327 | send_event: 0, 328 | display: ptr::null_mut(), 329 | window, 330 | message_type: self.get_atom(atom) as c_ulong, 331 | format: 32, 332 | data: data.into(), 333 | }; 334 | 335 | let event_ptr: *mut XClientMessageEvent = &mut event; 336 | unsafe { 337 | XSendEvent(self.display, self.root, 0, NoEventMask, event_ptr as *mut XEvent); 338 | } 339 | } 340 | 341 | pub fn delete_property(&self, window: Window, atom: A) { 342 | unsafe { 343 | XDeleteProperty(self.display, window, atom.into(self)); 344 | } 345 | } 346 | 347 | pub fn configure_window(&self, 348 | window: Window, 349 | window_changes: WindowChanges, 350 | mask: u32, 351 | floating: bool) { 352 | unsafe { 353 | if floating { 354 | let mut ret_window_changes = XWindowChanges { 355 | x: window_changes.x as i32, 356 | y: window_changes.y as i32, 357 | width: window_changes.width as i32, 358 | height: window_changes.height as i32, 359 | border_width: window_changes.border_width as i32, 360 | sibling: window_changes.sibling, 361 | stack_mode: window_changes.stack_mode as i32, 362 | }; 363 | XConfigureWindow(self.display, window, mask, &mut ret_window_changes); 364 | } else { 365 | let rect = self.get_geometry(window); 366 | let mut attributes = MaybeUninit::zeroed(); 367 | 368 | XGetWindowAttributes(self.display, window, attributes.as_mut_ptr()); 369 | 370 | let mut event = XConfigureEvent { 371 | type_: ConfigureNotify, 372 | display: self.display, 373 | serial: 0, 374 | send_event: 1, 375 | x: rect.x as i32, 376 | y: rect.y as i32, 377 | width: rect.width as i32, 378 | height: rect.height as i32, 379 | border_width: 0, 380 | event: window, 381 | window, 382 | above: 0, 383 | override_redirect: attributes.assume_init().override_redirect, 384 | }; 385 | let event_ptr: *mut XConfigureEvent = &mut event; 386 | XSendEvent(self.display, window, 0, 0, event_ptr as *mut XEvent); 387 | } 388 | XSync(self.display, 0); 389 | } 390 | } 391 | 392 | pub fn show_window(&self, window: Window) { 393 | unsafe { 394 | self.change_property(window, "WM_STATE", "WM_STATE", PropModeReplace, &[1u64, 0]); 395 | XMapWindow(self.display, window); 396 | } 397 | } 398 | 399 | pub fn hide_window(&self, window: Window) { 400 | unsafe { 401 | XSelectInput(self.display, window, 0x0040_0010 | FocusChangeMask); 402 | XUnmapWindow(self.display, window); 403 | XSelectInput(self.display, window, 0x0042_0010 | FocusChangeMask); 404 | 405 | self.change_property(window, "WM_STATE", "WM_STATE", PropModeReplace, &[0u64, 0]); 406 | self.delete_property(window, "_NET_WM_DESKTOP"); 407 | } 408 | } 409 | 410 | pub fn lower_window(&self, window: Window) { 411 | unsafe { 412 | XLowerWindow(self.display, window); 413 | } 414 | } 415 | 416 | pub fn raise_window(&self, window: Window) { 417 | unsafe { 418 | XRaiseWindow(self.display, window); 419 | } 420 | } 421 | 422 | pub fn unmap_window(&self, window: Window) { 423 | unsafe { 424 | XUnmapWindow(self.display, window); 425 | } 426 | } 427 | 428 | pub fn move_resize_window(&self, window: Window, x: u32, y: u32, width: u32, height: u32) { 429 | unsafe { 430 | XMoveResizeWindow(self.display, window, x as i32, y as i32, width, height); 431 | } 432 | } 433 | 434 | pub fn focus_window(&self, window: Window) { 435 | let input_hint = self.get_wm_hints(window).map(|x| x.input != 0).unwrap_or(true); 436 | let takes_focus = self.has_protocol(window, "WM_TAKE_FOCUS"); 437 | 438 | if input_hint { 439 | trace!("set input focus via hint"); 440 | unsafe { 441 | XSetInputFocus(self.display, window, 1, 0); 442 | self.skip_enter_events(); 443 | } 444 | 445 | ewmh::set_active_window(self, window); 446 | let state = self.get_atom("_NET_WM_STATE_DEMANDS_ATTENTION"); 447 | self.send_client_message(window, "_NET_WM_STATE", [0, state, 0, 0, 0]); 448 | } else if takes_focus { 449 | trace!("send WM_TAKE_FOCUS to: {:#x}", window); 450 | let time = SystemTime::now().duration_since(UNIX_EPOCH) 451 | .map(|x| x.as_secs()) 452 | .unwrap_or(0); 453 | 454 | let atom = self.get_atom("WM_TAKE_FOCUS"); 455 | let mut event = XClientMessageEvent { 456 | type_: ClientMessage, 457 | serial: 0, 458 | send_event: 0, 459 | display: ptr::null_mut(), 460 | window, 461 | message_type: self.get_atom("WM_PROTOCOLS") as c_ulong, 462 | format: 32, 463 | data: ClientMessageData::from([atom, time, 0, 0, 0]), 464 | }; 465 | 466 | let event_ptr: *mut XClientMessageEvent = &mut event; 467 | unsafe { 468 | XSendEvent(self.display, window, 0, NoEventMask, event_ptr as *mut XEvent); 469 | } 470 | let state = self.get_atom("_NET_WM_STATE_DEMANDS_ATTENTION"); 471 | 472 | ewmh::set_active_window(self, window); 473 | self.send_client_message(window, "_NET_WM_STATE", [0, state, 0, 0, 0]); 474 | } 475 | } 476 | 477 | pub fn skip_enter_events(&self) { 478 | unsafe { 479 | let event: *mut c_void = libc::malloc(256); 480 | XSync(self.display, 0); 481 | while XCheckMaskEvent(self.display, 16, event as *mut XEvent) != 0 { 482 | } 483 | libc::free(event); 484 | } 485 | } 486 | 487 | fn has_protocol(&self, window: Window, protocol: &str) -> bool { 488 | unsafe { 489 | let mut count: MaybeUninit = MaybeUninit::uninit(); 490 | let mut atoms: MaybeUninit<*mut Atom> = MaybeUninit::uninit(); 491 | 492 | if XGetWMProtocols(self.display, window, atoms.as_mut_ptr(), count.as_mut_ptr()) != 0 { 493 | let atoms = atoms.assume_init(); 494 | let count = count.assume_init(); 495 | let protocol_atom = self.get_atom(protocol); 496 | 497 | if count != 0 { 498 | let ret = from_raw_parts(atoms, count as usize) 499 | .contains(&protocol_atom); 500 | 501 | XFree(atoms as *mut c_void); 502 | 503 | return ret 504 | } 505 | } 506 | false 507 | } 508 | } 509 | 510 | pub fn kill_window(&self, window: Window) { 511 | if window == 0 { 512 | return; 513 | } 514 | 515 | unsafe { 516 | if self.has_protocol(window, "WM_DELETE_WINDOW") { 517 | let time = SystemTime::now().duration_since(UNIX_EPOCH) 518 | .map(|x| x.as_secs()) 519 | .unwrap_or(0); 520 | let delete_atom = self.get_atom("WM_DELETE_WINDOW"); 521 | 522 | let mut event = XClientMessageEvent { 523 | type_: ClientMessage, 524 | serial: 0, 525 | send_event: 0, 526 | display: ptr::null_mut(), 527 | window, 528 | message_type: self.get_atom("WM_PROTOCOLS") as c_ulong, 529 | format: 32, 530 | data: ClientMessageData::from([delete_atom, time, 0, 0, 0]), 531 | }; 532 | 533 | let event_ptr: *mut XClientMessageEvent = &mut event; 534 | XSendEvent(self.display, window, 0, NoEventMask, event_ptr as *mut XEvent); 535 | } else { 536 | XKillClient(self.display, window); 537 | XSync(self.display, 0); 538 | } 539 | } 540 | } 541 | 542 | pub fn restack_windows(&self, mut windows: Vec) { 543 | unsafe { 544 | XRestackWindows(self.display, 545 | (windows[..]).as_mut_ptr(), 546 | windows.len() as i32); 547 | } 548 | } 549 | 550 | pub fn grab_button(&self, window: Window) { 551 | unsafe { 552 | XGrabButton(self.display, 1, 0x8000, window, 1, 256, 0, 0, 0, 0); 553 | } 554 | } 555 | 556 | pub fn grab_modifier(&self, mod_key: u8) { 557 | unsafe { 558 | XGrabKey(self.display, 0, u32::from(mod_key), self.root, 1, 0, 1); 559 | XGrabKey(self.display, 560 | 0, 561 | u32::from(mod_key | MOD_2), 562 | self.root, 563 | 1, 564 | 0, 565 | 1); 566 | XGrabKey(self.display, 567 | 0, 568 | u32::from(mod_key | MOD_LOCK), 569 | self.root, 570 | 1, 571 | 0, 572 | 1); 573 | XGrabKey(self.display, 574 | 0, 575 | u32::from(mod_key | MOD_2 | MOD_LOCK), 576 | self.root, 577 | 1, 578 | 0, 579 | 1); 580 | } 581 | } 582 | 583 | pub fn keycode_to_string(&self, keycode: u32) -> String { 584 | unsafe { 585 | let keysym = XKeycodeToKeysym(self.display, keycode as u8, 0); 586 | str::from_utf8(CStr::from_ptr(XKeysymToString(keysym) as *const i8).to_bytes()) 587 | .unwrap() 588 | .to_string() 589 | } 590 | } 591 | 592 | pub fn set_window_border_width(&self, window: Window, width: u32) { 593 | if window != self.root { 594 | unsafe { 595 | XSetWindowBorderWidth(self.display, window, width); 596 | } 597 | } 598 | } 599 | 600 | pub fn set_window_border_color(&self, window: Window, color: u32) { 601 | if window != self.root { 602 | unsafe { 603 | XSetWindowBorder(self.display, window, u64::from(color)); 604 | } 605 | } 606 | } 607 | 608 | pub fn get_display_width(&self, screen: u32) -> u32 { 609 | unsafe { XDisplayWidth(self.display, screen as i32) as u32 } 610 | } 611 | 612 | pub fn get_display_height(&self, screen: u32) -> u32 { 613 | unsafe { XDisplayHeight(self.display, screen as i32) as u32 } 614 | } 615 | 616 | pub fn get_display_rect(&self) -> Rect { 617 | Rect { 618 | x: 0, 619 | y: 0, 620 | width: self.get_display_width(0), 621 | height: self.get_display_height(0), 622 | } 623 | } 624 | 625 | pub fn get_geometry(&self, window: Window) -> Rect { 626 | unsafe { 627 | let mut root = MaybeUninit::uninit(); 628 | let mut x = MaybeUninit::uninit(); 629 | let mut y = MaybeUninit::uninit(); 630 | let mut width = MaybeUninit::uninit(); 631 | let mut height = MaybeUninit::uninit(); 632 | let mut depth = MaybeUninit::uninit(); 633 | let mut border = MaybeUninit::uninit(); 634 | 635 | XGetGeometry(self.display, 636 | window, 637 | root.as_mut_ptr(), 638 | x.as_mut_ptr(), 639 | y.as_mut_ptr(), 640 | width.as_mut_ptr(), 641 | height.as_mut_ptr(), 642 | border.as_mut_ptr(), 643 | depth.as_mut_ptr()); 644 | 645 | Rect { 646 | x: x.assume_init() as u32, 647 | y: y.assume_init() as u32, 648 | width: width.assume_init(), 649 | height: height.assume_init(), 650 | } 651 | } 652 | } 653 | 654 | pub fn get_screen_infos(&self) -> Vec { 655 | unsafe { 656 | let mut num: c_int = 0; 657 | let screen_ptr = XineramaQueryScreens(self.display, &mut num); 658 | 659 | if num == 0 { 660 | return vec![self.get_display_rect()]; 661 | } 662 | 663 | let vec = from_raw_parts(screen_ptr, num as usize) 664 | .iter() 665 | .map(|screen_info| { 666 | Rect { 667 | x: screen_info.x_org as u32, 668 | y: screen_info.y_org as u32, 669 | width: screen_info.width as u32, 670 | height: screen_info.height as u32, 671 | } 672 | }) 673 | .collect(); 674 | XFree(screen_ptr as *mut c_void); 675 | vec 676 | } 677 | } 678 | 679 | pub fn is_floating_window(&self, window: Window) -> bool { 680 | if self.transient_for(window).is_some() { 681 | return true; 682 | } 683 | 684 | let hints = self.get_size_hints(window); 685 | let min = hints.min; 686 | let max = hints.max; 687 | 688 | if min.is_some() && max.is_some() && min.unwrap().0 == max.unwrap().0 && 689 | min.unwrap().1 == max.unwrap().1 { 690 | return true; 691 | } 692 | 693 | if let Some(property) = self.get_property(window, self.get_atom("_NET_WM_WINDOW_TYPE")) { 694 | let dialog = self.get_atom("_NET_WM_WINDOW_TYPE_DIALOG"); 695 | let splash = self.get_atom("_NET_WM_WINDOW_TYPE_SPLASH"); 696 | 697 | property.iter().any(|&x| x == dialog || x == splash) 698 | } else { 699 | false 700 | } 701 | } 702 | 703 | pub fn transient_for(&self, window: Window) -> Option { 704 | unsafe { 705 | let mut w = MaybeUninit::uninit(); 706 | 707 | if XGetTransientForHint(self.display, window, w.as_mut_ptr()) != 0 { 708 | Some(w.assume_init()) 709 | } else { 710 | None 711 | } 712 | } 713 | } 714 | 715 | pub fn get_size_hints(&self, window: Window) -> SizeHint { 716 | unsafe { 717 | let mut size_hint = MaybeUninit::zeroed(); 718 | let mut tmp: c_long = 0; 719 | XGetWMNormalHints(self.display, window, size_hint.as_mut_ptr(), &mut tmp); 720 | 721 | let size_hint = size_hint.assume_init(); 722 | let min = if (size_hint.flags & PMinSize) != 0 { 723 | Some((size_hint.min_width as u32, size_hint.min_height as u32)) 724 | } else { 725 | None 726 | }; 727 | 728 | let max = if (size_hint.flags & PMaxSize) != 0 { 729 | Some((size_hint.max_width as u32, size_hint.max_height as u32)) 730 | } else { 731 | None 732 | }; 733 | SizeHint { 734 | min, 735 | max, 736 | } 737 | } 738 | } 739 | 740 | pub fn get_wm_hints(&self, window: Window) -> Option { 741 | unsafe { 742 | let ptr = XGetWMHints(self.display, window); 743 | let ret = ptr.as_ref().copied(); 744 | XFree(ptr as *mut c_void); 745 | ret 746 | } 747 | } 748 | 749 | pub fn is_urgent(&self, window: Window) -> bool { 750 | self.get_wm_hints(window) 751 | .map(|x| x.flags & XUrgencyHint != 0) 752 | .unwrap_or(false) 753 | } 754 | 755 | pub fn get_class_name(&self, window: Window) -> Option { 756 | unsafe { 757 | let mut hint = MaybeUninit::uninit(); 758 | 759 | if XGetClassHint(self.display, window, hint.as_mut_ptr()) != 0 { 760 | let hint = hint.assume_init(); 761 | if !hint.res_class.is_null() { 762 | let hint_cstr = CStr::from_ptr(hint.res_class); 763 | let ret = str::from_utf8(hint_cstr.to_bytes()) 764 | .map(|x| x.to_owned()) 765 | .ok(); 766 | XFree(hint.res_class as *mut c_void); 767 | XFree(hint.res_name as *mut c_void); 768 | return ret; 769 | } 770 | } 771 | None 772 | } 773 | } 774 | 775 | pub fn get_window_title(&self, window: Window) -> String { 776 | if window == self.root { 777 | return String::new(); 778 | } 779 | 780 | unsafe { 781 | let mut name = MaybeUninit::uninit(); 782 | 783 | let ret = XGetTextProperty(self.display, window, name.as_mut_ptr(), self.get_atom("_NET_WM_NAME")); 784 | 785 | let ptr = if ret != 0 { 786 | name.assume_init().value as *const i8 787 | } else { 788 | let mut name = MaybeUninit::uninit(); 789 | 790 | if XFetchName(self.display, window, name.as_mut_ptr()) != 0 { 791 | name.assume_init() as *const i8 792 | } else { 793 | ptr::null() 794 | } 795 | }; 796 | 797 | if !ptr.is_null() { 798 | let ret_str = Self::ptr_to_string(ptr); 799 | XFree(ptr as *mut c_void); 800 | ret_str 801 | } else { 802 | String::new() 803 | } 804 | } 805 | } 806 | 807 | unsafe fn ptr_to_string(ptr: *const i8) -> String { 808 | match str::from_utf8(CStr::from_ptr(ptr).to_bytes()) { 809 | Ok(s) => s.to_string(), 810 | Err(_) => String::new(), 811 | } 812 | } 813 | 814 | pub fn move_pointer(&self, x: i32, y: i32) { 815 | unsafe { 816 | let mut root_w = MaybeUninit::uninit(); 817 | let mut child_w = MaybeUninit::uninit(); 818 | let mut root_x = MaybeUninit::uninit(); 819 | let mut root_y = MaybeUninit::uninit(); 820 | let mut win_x = MaybeUninit::uninit(); 821 | let mut win_y = MaybeUninit::uninit(); 822 | let mut mask = MaybeUninit::uninit(); 823 | 824 | let ret = XQueryPointer( 825 | self.display, 826 | self.root, 827 | root_w.as_mut_ptr() as *mut Window, 828 | child_w.as_mut_ptr() as *mut Window, 829 | root_x.as_mut_ptr(), 830 | root_y.as_mut_ptr(), 831 | win_x.as_mut_ptr(), 832 | win_y.as_mut_ptr(), 833 | mask.as_mut_ptr()); 834 | 835 | if ret == 1 { 836 | XWarpPointer(self.display, 0, 0, 0, 0, 0, 0, x - root_x.assume_init(), y - root_y.assume_init()); 837 | } 838 | } 839 | } 840 | 841 | pub fn request_window_events(&self, window: Window) { 842 | unsafe { 843 | self.grab_button(window); 844 | XSelectInput(self.display, window, 0x0042_0010 | FocusChangeMask); 845 | } 846 | } 847 | 848 | fn cast_event_to(&self) -> &T { 849 | unsafe { &*(self.event as *const T) } 850 | } 851 | 852 | #[allow(clippy::nonminimal_bool)] 853 | pub fn get_event(&self) -> XlibEvent { 854 | if self.display.is_null() { 855 | return WMClose; 856 | } 857 | 858 | unsafe { 859 | XNextEvent(self.display, self.event as *mut XEvent); 860 | } 861 | 862 | let evt_type: c_int = *self.cast_event_to(); 863 | match evt_type { 864 | /* 865 | * MapRequest is triggered whenever a client initiates a map window request on an unmapped window 866 | * whose override_redirect is set to false 867 | */ 868 | MapRequest => { 869 | let evt: &XMapRequestEvent = self.cast_event_to(); 870 | trace!("MapRequest {:?}", evt); 871 | 872 | // Some docks rely entirely on the EWMH window type and do not set redirect 873 | // override to prevent the WM from reparenting it 874 | let dock_type = self.get_atom("_NET_WM_WINDOW_TYPE_DOCK"); 875 | let is_dock = self.get_property(evt.window, "_NET_WM_WINDOW_TYPE") 876 | .map(|atoms| atoms.iter().any(|&a| a == dock_type)) 877 | .unwrap_or(false); 878 | 879 | let sticky_type = self.get_atom("_NET_WM_STATE_STICKY"); 880 | let is_sticky = self.get_property(evt.window, "_NET_WM_STATE") 881 | .map(|atoms| atoms.iter().any(|&a| a == sticky_type)) 882 | .unwrap_or(false); 883 | 884 | if is_dock { 885 | // map the dock but do not manage it any further 886 | unsafe { 887 | XMapWindow(self.display, evt.window); 888 | } 889 | Ignored 890 | } else { 891 | self.request_window_events(evt.window); 892 | 893 | XMapRequest(evt.window, is_sticky) 894 | } 895 | } 896 | /* 897 | * MapNotify is triggered whenever a client is mapped regardless of the value of override_redirect. 898 | * Compard to MapRequest this also catches windows that are not suppose to be managed 899 | * by the WM. 900 | */ 901 | MapNotify => { 902 | let evt: &XMapEvent = self.cast_event_to(); 903 | trace!("MapNotify: {:?}", evt); 904 | 905 | // only windows with override redirect dont have WM_STATE 906 | if self.get_property(evt.window, "WM_STATE").is_none() { 907 | unsafe { 908 | XSelectInput(self.display, evt.window, PropertyChangeMask); 909 | } 910 | } 911 | Ignored 912 | } 913 | ClientMessage => { 914 | let evt: &XClientMessageEvent = self.cast_event_to(); 915 | XClientMessage(evt.window, evt.message_type, evt.data) 916 | } 917 | ConfigureNotify => { 918 | let evt: &XConfigureEvent = self.cast_event_to(); 919 | if evt.window == self.root { 920 | XConfigureNotify(evt.window) 921 | } else { 922 | Ignored 923 | } 924 | } 925 | ConfigureRequest => { 926 | let event: &XConfigureRequestEvent = self.cast_event_to(); 927 | let changes = WindowChanges { 928 | x: event.x as u32, 929 | y: event.y as u32, 930 | width: event.width as u32, 931 | height: event.height as u32, 932 | border_width: event.border_width as u32, 933 | sibling: event.above as Window, 934 | stack_mode: event.detail as u32, 935 | }; 936 | XConfigureRequest(event.window, changes, event.value_mask as u32) 937 | } 938 | DestroyNotify => { 939 | let evt: &XDestroyWindowEvent = self.cast_event_to(); 940 | XDestroy(evt.window) 941 | } 942 | UnmapNotify => { 943 | let evt: &XUnmapEvent = self.cast_event_to(); 944 | XUnmapNotify(evt.window, evt.send_event > 0) 945 | } 946 | PropertyNotify => { 947 | let evt: &XPropertyEvent = self.cast_event_to(); 948 | XPropertyNotify(evt.window, evt.atom, evt.state == 0) 949 | } 950 | EnterNotify => { 951 | let evt: &XEnterWindowEvent = self.cast_event_to(); 952 | if evt.detail != 2 { 953 | XEnterNotify(evt.window, false, evt.x as u32, evt.y as u32) 954 | } else if evt.detail == 2 && evt.window == self.root { 955 | XEnterNotify(evt.window, true, evt.x as u32, evt.y as u32) 956 | } else { 957 | Ignored 958 | } 959 | } 960 | FocusIn => { 961 | let evt: &XFocusInEvent = self.cast_event_to(); 962 | trace!("XFocusIn: {:?}", evt); 963 | if (evt.mode == NotifyNormal && 964 | // mouse focus move from root to window 965 | (evt.detail == NotifyAncestor || 966 | // mouse focus move from window to window 967 | evt.detail == NotifyNonlinear)) || 968 | (evt.mode == NotifyWhileGrabbed && 969 | // manual focus move from root to window 970 | (evt.detail == NotifyAncestor || 971 | // manual focus move from window to window 972 | evt.detail == NotifyNonlinear)) 973 | { 974 | XFocusIn(evt.window) 975 | } else { 976 | Ignored 977 | } 978 | } 979 | ButtonPress => { 980 | let evt: &XButtonPressedEvent = self.cast_event_to(); 981 | unsafe { 982 | XAllowEvents(self.display, 2, 0); 983 | } 984 | 985 | XButtonPress(evt.window) 986 | } 987 | KeyPress => { 988 | let evt: &XKeyPressedEvent = self.cast_event_to(); 989 | XKeyPress(evt.window, 990 | evt.state as u8, 991 | self.keycode_to_string(evt.keycode)) 992 | } 993 | _ => Ignored, 994 | } 995 | } 996 | } 997 | 998 | impl Default for XlibWindowSystem { 999 | fn default() -> Self { 1000 | Self::new() 1001 | } 1002 | } 1003 | 1004 | pub trait IntoAtom { 1005 | fn into(self, xws: &XlibWindowSystem) -> Atom; 1006 | } 1007 | 1008 | impl IntoAtom for Atom { 1009 | fn into(self, _: &XlibWindowSystem) -> Atom { 1010 | self 1011 | } 1012 | } 1013 | 1014 | impl IntoAtom for &str { 1015 | fn into(self, xws: &XlibWindowSystem) -> Atom { 1016 | xws.get_atom(self) 1017 | } 1018 | } 1019 | -------------------------------------------------------------------------------- /src/xr3wm.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use anyhow::{Context, Result}; 5 | use config::Config; 6 | use state::WmState; 7 | use xlib_window_system::XlibWindowSystem; 8 | use xlib_window_system::XlibEvent::*; 9 | use std::env; 10 | 11 | mod commands; 12 | mod config; 13 | mod ewmh; 14 | mod keycode; 15 | mod layout; 16 | mod stack; 17 | mod state; 18 | mod statusbar; 19 | mod utils; 20 | mod workspace; 21 | mod xlib_window_system; 22 | 23 | fn print_version() -> ! { 24 | println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 25 | ::std::process::exit(0); 26 | } 27 | 28 | fn print_help() -> ! { 29 | println!("usage: xr3wm [OPTION] 30 | Xmonad and i3 inspired X11 tiling window manager. 31 | 32 | -c, --config=path config file path 33 | -h, --help display this help and exit 34 | -v, --version output version information and exit"); 35 | ::std::process::exit(0); 36 | } 37 | 38 | fn handle_args() { 39 | let args = env::args().skip(1); 40 | 41 | for arg in args { 42 | match arg.as_str() { 43 | "--help" | "-h" => print_help(), 44 | "--version" | "-v" => print_version(), 45 | _ => (), 46 | } 47 | } 48 | } 49 | 50 | fn run() -> Result<()> { 51 | handle_args(); 52 | 53 | // initialize logging system 54 | env_logger::init(); 55 | 56 | info!("loading config"); 57 | 58 | let (config, ws_cfg_list) = Config::load() 59 | .context("failed to load config")?; 60 | 61 | info!("initializing Xlib"); 62 | let xws = &mut XlibWindowSystem::new(); 63 | xws.init(); 64 | xws.grab_modifier(config.mod_key); 65 | 66 | let mut state = WmState::new(ws_cfg_list, xws) 67 | .context("failed to create initial wm state")?; 68 | 69 | state.rescreen(xws, &config); 70 | 71 | ewmh::set_current_desktop(xws, state.get_ws_index()); 72 | ewmh::set_number_of_desktops(xws, state.ws_count()); 73 | ewmh::set_desktop_names(xws, state.all_ws()); 74 | ewmh::set_desktop_viewport(xws, state.all_ws()); 75 | 76 | info!("entering event loop"); 77 | run_event_loop(config, xws, state) 78 | } 79 | 80 | fn run_event_loop(config: Config, xws: &mut XlibWindowSystem, mut state: WmState) -> Result<()> { 81 | let mut bar_handle = config.statusbar 82 | .as_ref() 83 | .map(|bar| bar.start()) 84 | .transpose() 85 | .context("failed to start statusbar")?; 86 | 87 | loop { 88 | match xws.get_event() { 89 | XMapRequest(window, is_sticky) => { 90 | trace!("XMapRequest: {:#x} {}", window, is_sticky); 91 | if !state.contains(window) { 92 | let mut is_hooked = false; 93 | if let Some(class) = xws.get_class_name(window) { 94 | for hook in config.manage_hooks.iter() { 95 | if hook.class_name == class { 96 | hook.cmd.call(xws, &mut state, &config, window); 97 | is_hooked = true; 98 | } 99 | } 100 | } 101 | 102 | if !is_hooked { 103 | state.add_window(None, xws, &config, window); 104 | } 105 | 106 | state.focus_window(xws, &config, window, false); 107 | } 108 | } 109 | XDestroy(window) => { 110 | trace!("XDestroy: {:#x}", window); 111 | if state.contains(window) { 112 | state.remove_window(xws, &config, window); 113 | } 114 | } 115 | XUnmapNotify(window, send) => { 116 | trace!("XUnmapNotify: {:#x} {}", window, send); 117 | if send && state.contains(window) { 118 | state.remove_window(xws, &config, window); 119 | } else if state.try_remove_unmanaged(window) { 120 | state.redraw(xws, &config); 121 | } 122 | } 123 | XPropertyNotify(window, atom, _is_new_value) => { 124 | if atom == xws.get_atom("WM_HINTS") { 125 | if let Some(ws) = state.get_parent_mut(window) { 126 | ws.set_urgency(xws.is_urgent(window), window); 127 | } 128 | } else if atom == xws.get_atom("_NET_WM_STRUT_PARTIAL") { 129 | state.redraw(xws, &config); 130 | } else if window == xws.get_root_window() && 131 | (atom == xws.get_atom("_NET_CURRENT_DESKTOP") || 132 | atom == xws.get_atom("_NET_NUMBER_OF_DESKTOPS") || 133 | atom == xws.get_atom("_NET_DESKTOP_NAMES") || 134 | atom == xws.get_atom("_NET_ACTIVE_WINDOW") || 135 | atom == xws.get_atom("_NET_WM_STATE") || 136 | atom == xws.get_atom("_NET_WM_NAME")) 137 | { 138 | if let Some(ref mut handle) = bar_handle { 139 | if let Err(e) = config.statusbar.as_ref().unwrap().update(handle, xws, &state) { 140 | error!("{}", e.context("failed to update statusbar")); 141 | } 142 | } 143 | } 144 | } 145 | XClientMessage(window, msg_type, data) => { 146 | let data: Vec = data.as_longs().iter().map(|x| *x as u64).collect(); 147 | trace!("ClientMessage: {:#x} {}, {:?}", window, xws.get_atom_name(msg_type), data); 148 | ewmh::process_client_message(&mut state, xws, &config, window, msg_type, &data); 149 | } 150 | XConfigureNotify(_) => { 151 | trace!("XConfigurationNotify"); 152 | state.rescreen(xws, &config); 153 | } 154 | XConfigureRequest(window, changes, mask) => { 155 | trace!("XConfigureRequest: {:#x}", window); 156 | let is_floating = state.is_floating(window) || !state.contains(window); 157 | xws.configure_window(window, changes, mask, is_floating); 158 | } 159 | XEnterNotify(window, is_root, x, y) => { 160 | trace!("XEnterNotify: {:#x} {} x: {} y: {}", window, is_root,x ,y); 161 | if is_root { 162 | state.switch_to_ws_at(xws, &config, x, y, false) 163 | } else { 164 | state.focus_window(xws, &config, window, false); 165 | } 166 | } 167 | XFocusIn(window) => { 168 | trace!("Focus event by: {:#x}", window); 169 | if let Some(idx) = state.find_window(window) { 170 | let screens = state.get_screens(); 171 | if let Some(workspace) = state.get_ws(idx) { 172 | workspace.redraw(xws, &config, screens); 173 | state.raise_sticky(xws); 174 | } 175 | } 176 | } 177 | XButtonPress(window) => { 178 | state.focus_window(xws, &config, window, false); 179 | } 180 | XKeyPress(_, mods, key) => { 181 | trace!("XKeyPress: {}, {}", mods, key); 182 | let mods = mods & !(config.mod_key | 0b10010); 183 | 184 | for (binding, cmd) in config.keybindings.iter() { 185 | if binding.mods == mods && binding.key == key { 186 | cmd.call(xws, &mut state, &config, bar_handle.as_mut()) 187 | .map_err(|e| error!("{}", utils::concat_error_chain(&e))) 188 | .ok(); 189 | } 190 | } 191 | } 192 | WMClose => return Ok(()), 193 | _ => {} 194 | } 195 | } 196 | } 197 | 198 | fn main() { 199 | // failure crate boilerplate 200 | if let Err(e) = run() { 201 | use std::io::Write; 202 | 203 | let mut stderr = std::io::stderr(); 204 | 205 | if log_enabled!(log::Level::Error) { 206 | error!("{}", e); 207 | } else { 208 | writeln!(stderr, "ERROR: {e}").ok(); 209 | } 210 | 211 | e.chain().skip(1) 212 | .for_each(|cause| error!("because: {}", cause)); 213 | 214 | error!("backtrace: {}", e.backtrace()); 215 | 216 | stderr.flush().ok(); 217 | ::std::process::exit(1); 218 | } 219 | } 220 | --------------------------------------------------------------------------------