Diff
diff --git a/.gitignore b/.gitignore
index 8bd4198..c558b7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,11 @@
-# Rust / Cargo
-target/
+# Zig
+zig-out/
+.zig-cache/
.direnv/
# Nix
result
-# User config
-src/config.rs
-
# Notes
notes/
*.md
diff --git a/Cargo.lock b/Cargo.lock
deleted file mode 100644
index c793f6d..0000000
--- a/Cargo.lock
+++ /dev/null
@@ -1,730 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "android_system_properties"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "autocfg"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
-
-[[package]]
-name = "bitflags"
-version = "2.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
-
-[[package]]
-name = "bstr"
-version = "1.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
-dependencies = [
- "memchr",
- "serde",
-]
-
-[[package]]
-name = "bumpalo"
-version = "3.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
-
-[[package]]
-name = "cc"
-version = "1.2.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
-dependencies = [
- "find-msvc-tools",
- "shlex",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "chrono"
-version = "0.4.42"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
-dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-
-[[package]]
-name = "dirs"
-version = "5.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
-dependencies = [
- "dirs-sys",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "either"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
-
-[[package]]
-name = "env_home"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
-
-[[package]]
-name = "errno"
-version = "0.3.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
-dependencies = [
- "libc",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "find-msvc-tools"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
-
-[[package]]
-name = "gethostname"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
-dependencies = [
- "rustix",
- "windows-link",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi",
-]
-
-[[package]]
-name = "iana-time-zone"
-version = "0.1.64"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "js-sys"
-version = "0.3.81"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
-dependencies = [
- "once_cell",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.177"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
-
-[[package]]
-name = "libredox"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
-dependencies = [
- "bitflags",
- "libc",
-]
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
-
-[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
-
-[[package]]
-name = "lua-src"
-version = "547.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "luajit-src"
-version = "210.5.12+a4f56a4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
-dependencies = [
- "cc",
- "which",
-]
-
-[[package]]
-name = "memchr"
-version = "2.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
-
-[[package]]
-name = "mlua"
-version = "0.10.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0"
-dependencies = [
- "bstr",
- "either",
- "mlua-sys",
- "num-traits",
- "parking_lot",
- "rustc-hash",
- "rustversion",
-]
-
-[[package]]
-name = "mlua-sys"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93"
-dependencies = [
- "cc",
- "cfg-if",
- "lua-src",
- "luajit-src",
- "pkg-config",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.21.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
-
-[[package]]
-name = "option-ext"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
-
-[[package]]
-name = "oxwm"
-version = "0.10.2"
-dependencies = [
- "chrono",
- "dirs",
- "mlua",
- "serde",
- "x11",
- "x11rb",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
-[[package]]
-name = "pkg-config"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.101"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "redox_users"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
-dependencies = [
- "getrandom",
- "libredox",
- "thiserror",
-]
-
-[[package]]
-name = "rustc-hash"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
-
-[[package]]
-name = "rustix"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
-name = "serde"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
-
-[[package]]
-name = "serde_core"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "shlex"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "smallvec"
-version = "1.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
-
-[[package]]
-name = "syn"
-version = "2.0.107"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
-
-[[package]]
-name = "wasi"
-version = "0.11.1+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
-dependencies = [
- "cfg-if",
- "once_cell",
- "rustversion",
- "wasm-bindgen-macro",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
-dependencies = [
- "bumpalo",
- "log",
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "which"
-version = "7.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
-dependencies = [
- "either",
- "env_home",
- "rustix",
- "winsafe",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.62.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
-dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-link",
- "windows-result",
- "windows-strings",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.59.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "windows-link"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-
-[[package]]
-name = "windows-result"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.61.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
-
-[[package]]
-name = "winsafe"
-version = "0.0.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
-
-[[package]]
-name = "x11"
-version = "2.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
-dependencies = [
- "libc",
- "pkg-config",
-]
-
-[[package]]
-name = "x11rb"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
-dependencies = [
- "gethostname",
- "rustix",
- "x11rb-protocol",
- "xcursor",
-]
-
-[[package]]
-name = "x11rb-protocol"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
-
-[[package]]
-name = "xcursor"
-version = "0.3.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
diff --git a/Cargo.toml b/Cargo.toml
deleted file mode 100644
index 1453242..0000000
--- a/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name = "oxwm"
-version = "0.10.2"
-edition = "2024"
-
-[lib]
-name = "oxwm"
-path = "src/lib.rs"
-
-[[bin]]
-name = "oxwm"
-path = "src/bin/main.rs"
-
-[dependencies]
-x11 = { version = "2.21", features = ["xlib", "xft"] }
-x11rb = { version = "0.13", features = ["cursor", "xinerama"] }
-chrono = "0.4"
-dirs = "5.0"
-serde = { version = "1.0", features = ["derive"] }
-mlua = { version = "0.10", features = ["lua54", "vendored"] }
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..493ee08
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,76 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ const exe = b.addExecutable(.{
+ .name = "oxwm",
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ }),
+ });
+
+ exe.linkSystemLibrary("lua");
+ exe.linkSystemLibrary("X11");
+ exe.linkSystemLibrary("Xinerama");
+ exe.linkSystemLibrary("Xft");
+ exe.linkSystemLibrary("fontconfig");
+ exe.linkLibC();
+
+ b.installArtifact(exe);
+
+ const run_step = b.step("run", "Run oxwm");
+ const run_cmd = b.addRunArtifact(exe);
+ run_step.dependOn(&run_cmd.step);
+ run_cmd.step.dependOn(b.getInstallStep());
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ const test_step = b.step("test", "Run unit tests");
+ const unit_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("tests/main_tests.zig"),
+ .target = target,
+ .optimize = optimize,
+ }),
+ });
+ test_step.dependOn(&b.addRunArtifact(unit_tests).step);
+
+ const xephyr_step = b.step("xephyr", "Run in Xephyr (1280x800 on :2)");
+ xephyr_step.dependOn(&add_xephyr_run(b, exe, false).step);
+
+ const xephyr_multi_step = b.step("xephyr-multi", "Run in Xephyr multi-monitor on :2");
+ xephyr_multi_step.dependOn(&add_xephyr_run(b, exe, true).step);
+
+ const multimon_step = b.step("multimon", "Alias for xephyr-multi");
+ multimon_step.dependOn(&add_xephyr_run(b, exe, true).step);
+
+ const kill_step = b.step("kill", "Kill Xephyr and oxwm");
+ kill_step.dependOn(&b.addSystemCommand(&.{ "sh", "-c", "pkill -9 Xephyr || true; pkill -9 oxwm || true" }).step);
+
+ const fmt_step = b.step("fmt", "Format source files");
+ fmt_step.dependOn(&b.addFmt(.{ .paths = &.{"src/"} }).step);
+
+ const clean_step = b.step("clean", "Remove build artifacts");
+ clean_step.dependOn(&b.addSystemCommand(&.{ "rm", "-rf", "zig-out", ".zig-cache" }).step);
+}
+
+fn add_xephyr_run(b: *std.Build, exe: *std.Build.Step.Compile, multimon: bool) *std.Build.Step.Run {
+ const kill_cmd = if (multimon)
+ "pkill -9 Xephyr || true; Xephyr +xinerama -glamor -screen 640x480 -screen 640x480 :2 & sleep 1"
+ else
+ "pkill -9 Xephyr || true; Xephyr -screen 1280x800 :2 & sleep 1";
+
+ const setup = b.addSystemCommand(&.{ "sh", "-c", kill_cmd });
+
+ const run_wm = b.addRunArtifact(exe);
+ run_wm.step.dependOn(&setup.step);
+ run_wm.setEnvironmentVariable("DISPLAY", ":2");
+ run_wm.addArgs(&.{ "-c", "resources/config.lua" });
+
+ return run_wm;
+}
diff --git a/default.nix b/default.nix
index 51969c2..46e2a2d 100644
--- a/default.nix
+++ b/default.nix
@@ -1,45 +1,54 @@
{
lib,
- rustPlatform,
+ stdenv,
+ zig,
pkg-config,
- libX11,
- libXft,
- libXrender,
+ xorg,
+ lua5_4,
freetype,
fontconfig,
- gitRev ? "unkown",
+ gitRev ? "unknown",
}:
-rustPlatform.buildRustPackage (finalAttrs: {
+stdenv.mkDerivation (finalAttrs: {
pname = "oxwm";
version = "${lib.substring 0 8 gitRev}";
src = ./.;
- cargoLock.lockFile = ./Cargo.lock;
-
- nativeBuildInputs = [pkg-config];
+ nativeBuildInputs = [zig pkg-config];
buildInputs = [
- libX11
- libXft
- libXrender
+ xorg.libX11
+ xorg.libXinerama
+ xorg.libXft
+ lua5_4
freetype
fontconfig
];
- # tests require a running X server
- doCheck = false;
+ dontConfigure = true;
+
+ buildPhase = ''
+ runHook preBuild
+ zig build -Doptimize=ReleaseSafe --prefix $out
+ runHook postBuild
+ '';
- postInstall = ''
+ installPhase = ''
+ runHook preInstall
install resources/oxwm.desktop -Dt $out/share/xsessions
install -Dm644 resources/oxwm.1 -t $out/share/man/man1
install -Dm644 templates/oxwm.lua -t $out/share/oxwm
+ runHook postInstall
'';
+ # tests require a running X server
+ doCheck = false;
+
passthru.providedSessions = ["oxwm"];
meta = {
- description = "Dynamic window manager written in Rust, inspired by dwm";
+ description = "Dynamic window manager written in Zig, inspired by dwm";
homepage = "https://github.com/tonybanters/oxwm";
license = lib.licenses.gpl3Only;
platforms = lib.platforms.linux;
diff --git a/flake.nix b/flake.nix
index d266f94..39e64d0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -23,9 +23,8 @@
default = pkgs.mkShell {
inputsFrom = [self.packages.${pkgs.stdenv.hostPlatform.system}.oxwm];
packages = [
- pkgs.rustc
- pkgs.cargo
- pkgs.clippy
+ pkgs.zig
+ pkgs.zls
pkgs.alacritty
pkgs.just
pkgs.xorg.xorgserver
@@ -33,7 +32,6 @@
shellHook = ''
export PS1="(oxwm-dev) $PS1"
'';
- env.RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
};
});
diff --git a/justfile b/justfile
index 3befb6f..178a5c1 100644
--- a/justfile
+++ b/justfile
@@ -1,52 +1,43 @@
build:
- cargo build --release
+ zig build -Doptimize=ReleaseSafe
install: build
- cp target/release/oxwm /usr/bin/oxwm
- cp resources/oxwm.desktop /usr/share/xsessions/oxwm.desktop
- chmod +x /usr/bin/oxwm
- @echo "✓ oxwm installed to /usr/bin/oxwm"
- @echo " Run 'oxwm --init' to create your config"
+ sudo cp zig-out/bin/oxwm /usr/bin/oxwm
+ sudo cp resources/oxwm.desktop /usr/share/xsessions/oxwm.desktop
+ sudo chmod +x /usr/bin/oxwm
+ @echo "oxwm installed to /usr/bin/oxwm"
checkinstall:
checkinstall --pkgname oxwm --exclude /root -y just install
uninstall:
- rm -f /usr/bin/oxwm
- @echo "✓ oxwm uninstalled"
- @echo " Your config at ~/.config/oxwm/ is preserved"
+ sudo rm -f /usr/bin/oxwm
+ @echo "oxwm uninstalled"
+ @echo "Your config at ~/.config/oxwm/ is preserved"
clean:
- cargo clean
+ rm -rf zig-out .zig-cache
test-clean:
pkill Xephyr || true
rm -rf ~/.config/oxwm
Xephyr -screen 1280x800 :1 & sleep 1
- DISPLAY=:1 cargo run --release -- --config resources/test-config.lua
+ DISPLAY=:1 zig build run -- -c resources/config.lua
test:
- pkill Xephyr || true
- Xephyr -screen 1280x800 :2 & sleep 1
- DISPLAY=:2 cargo run --release -- --config resources/test-config.lua
+ zig build xephyr
test-multimon:
- pkill Xephyr || true
- Xephyr +xinerama -screen 640x480 -screen 640x480 :1 & sleep 1
- DISPLAY=:1 cargo run --release -- --config resources/test-config.lua
-
-init:
- cargo run --release -- --init
+ zig build xephyr-multi
edit:
$EDITOR ~/.config/oxwm/config.lua
-check:
- cargo clippy -- -W clippy::all
- cargo fmt -- --check
-
fmt:
- cargo fmt
+ zig build fmt
+
+pre-commit: fmt build
+ @echo "All checks passed!"
-pre-commit: fmt check build
- @echo "✓ All checks passed!"
+run:
+ zig build run
diff --git a/src/animations.zig b/src/animations.zig
new file mode 100644
index 0000000..383be90
--- /dev/null
+++ b/src/animations.zig
@@ -0,0 +1,76 @@
+const std = @import("std");
+
+pub const Easing = enum {
+ linear,
+ ease_out,
+ ease_in_out,
+
+ pub fn apply(self: Easing, t: f64) f64 {
+ return switch (self) {
+ .linear => t,
+ .ease_out => 1.0 - std.math.pow(f64, 1.0 - t, 3),
+ .ease_in_out => if (t < 0.5)
+ 4.0 * t * t * t
+ else
+ 1.0 - std.math.pow(f64, -2.0 * t + 2.0, 3) / 2.0,
+ };
+ }
+};
+
+pub const Animation_Config = struct {
+ duration_ms: u64 = 150,
+ easing: Easing = .ease_out,
+};
+
+pub const Scroll_Animation = struct {
+ start_value: i32 = 0,
+ end_value: i32 = 0,
+ start_time: i64 = 0,
+ duration_ms: u64 = 150,
+ easing: Easing = .ease_out,
+ active: bool = false,
+
+ pub fn start(self: *Scroll_Animation, from: i32, to: i32, config: Animation_Config) void {
+ if (from == to) {
+ self.active = false;
+ return;
+ }
+ self.start_value = from;
+ self.end_value = to;
+ self.start_time = std.time.milliTimestamp();
+ self.duration_ms = config.duration_ms;
+ self.easing = config.easing;
+ self.active = true;
+ }
+
+ pub fn update(self: *Scroll_Animation) ?i32 {
+ if (!self.active) return null;
+
+ const now = std.time.milliTimestamp();
+ const elapsed = now - self.start_time;
+
+ if (elapsed >= @as(i64, @intCast(self.duration_ms))) {
+ self.active = false;
+ return self.end_value;
+ }
+
+ const t = @as(f64, @floatFromInt(elapsed)) / @as(f64, @floatFromInt(self.duration_ms));
+ const eased = self.easing.apply(t);
+ const diff = @as(f64, @floatFromInt(self.end_value - self.start_value));
+ const current = @as(f64, @floatFromInt(self.start_value)) + (diff * eased);
+
+ return @intFromFloat(current);
+ }
+
+ pub fn is_active(self: *const Scroll_Animation) bool {
+ return self.active;
+ }
+
+ pub fn target(self: *const Scroll_Animation) i32 {
+ return self.end_value;
+ }
+
+ pub fn stop(self: *Scroll_Animation) void {
+ self.active = false;
+ }
+};
diff --git a/src/animations/mod.rs b/src/animations/mod.rs
deleted file mode 100644
index 4b9ae69..0000000
--- a/src/animations/mod.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-mod scroll;
-
-pub use scroll::ScrollAnimation;
-
-use std::time::Duration;
-
-#[derive(Debug, Clone, Copy)]
-pub enum Easing {
- Linear,
- EaseOut,
- EaseInOut,
-}
-
-impl Easing {
- pub fn apply(&self, t: f64) -> f64 {
- match self {
- Easing::Linear => t,
- Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
- Easing::EaseInOut => {
- if t < 0.5 {
- 4.0 * t * t * t
- } else {
- 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
- }
- }
- }
- }
-}
-
-pub struct AnimationConfig {
- pub duration: Duration,
- pub easing: Easing,
-}
-
-impl Default for AnimationConfig {
- fn default() -> Self {
- Self {
- duration: Duration::from_millis(150),
- easing: Easing::EaseOut,
- }
- }
-}
diff --git a/src/animations/scroll.rs b/src/animations/scroll.rs
deleted file mode 100644
index 808b0eb..0000000
--- a/src/animations/scroll.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-use std::time::Instant;
-use super::{AnimationConfig, Easing};
-
-pub struct ScrollAnimation {
- start_value: i32,
- end_value: i32,
- start_time: Instant,
- duration_ms: u64,
- easing: Easing,
- active: bool,
-}
-
-impl ScrollAnimation {
- pub fn new() -> Self {
- Self {
- start_value: 0,
- end_value: 0,
- start_time: Instant::now(),
- duration_ms: 150,
- easing: Easing::EaseOut,
- active: false,
- }
- }
-
- pub fn start(&mut self, from: i32, to: i32, config: &AnimationConfig) {
- if from == to {
- self.active = false;
- return;
- }
- self.start_value = from;
- self.end_value = to;
- self.start_time = Instant::now();
- self.duration_ms = config.duration.as_millis() as u64;
- self.easing = config.easing;
- self.active = true;
- }
-
- pub fn update(&mut self) -> Option<i32> {
- if !self.active {
- return None;
- }
-
- let elapsed = self.start_time.elapsed().as_millis() as u64;
-
- if elapsed >= self.duration_ms {
- self.active = false;
- return Some(self.end_value);
- }
-
- let t = elapsed as f64 / self.duration_ms as f64;
- let eased_t = self.easing.apply(t);
-
- let delta = (self.end_value - self.start_value) as f64;
- let current = self.start_value as f64 + delta * eased_t;
-
- Some(current.round() as i32)
- }
-
- pub fn is_active(&self) -> bool {
- self.active
- }
-
- pub fn target(&self) -> i32 {
- self.end_value
- }
-
- pub fn cancel(&mut self) {
- self.active = false;
- }
-}
-
-impl Default for ScrollAnimation {
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/src/bar/bar.rs b/src/bar/bar.rs
deleted file mode 100644
index 9f74896..0000000
--- a/src/bar/bar.rs
+++ /dev/null
@@ -1,453 +0,0 @@
-use super::blocks::Block;
-use super::font::{DrawingSurface, Font};
-use crate::Config;
-use crate::errors::X11Error;
-use std::time::Instant;
-use x11rb::COPY_DEPTH_FROM_PARENT;
-use x11rb::connection::Connection;
-use x11rb::protocol::xproto::*;
-use x11rb::rust_connection::RustConnection;
-
-pub struct Bar {
- window: Window,
- width: u16,
- height: u16,
- graphics_context: Gcontext,
- surface: DrawingSurface,
-
- tag_widths: Vec<u16>,
- needs_redraw: bool,
-
- blocks: Vec<Box<dyn Block>>,
- block_last_updates: Vec<Instant>,
- block_underlines: Vec<bool>,
- status_text: String,
-
- tags: Vec<String>,
- scheme_normal: crate::ColorScheme,
- scheme_occupied: crate::ColorScheme,
- scheme_selected: crate::ColorScheme,
- scheme_urgent: crate::ColorScheme,
- hide_vacant_tags: bool,
- last_occupied_tags: u32,
- last_current_tags: u32,
-}
-
-impl Bar {
- pub fn new(
- connection: &RustConnection,
- screen: &Screen,
- screen_num: usize,
- config: &Config,
- display: *mut x11::xlib::Display,
- font: &Font,
- x: i16,
- y: i16,
- width: u16,
- cursor: u32,
- ) -> Result<Self, X11Error> {
- let window = connection.generate_id()?;
- let graphics_context = connection.generate_id()?;
-
- let height = (font.height() as f32 * 1.4) as u16;
-
- connection.create_window(
- COPY_DEPTH_FROM_PARENT,
- window,
- screen.root,
- x,
- y,
- width,
- height,
- 0,
- WindowClass::INPUT_OUTPUT,
- screen.root_visual,
- &CreateWindowAux::new()
- .background_pixel(config.scheme_normal.background)
- .event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS)
- .override_redirect(1),
- )?;
-
- connection.create_gc(
- graphics_context,
- window,
- &CreateGCAux::new()
- .foreground(config.scheme_normal.foreground)
- .background(config.scheme_normal.background),
- )?;
-
- unsafe {
- x11::xlib::XDefineCursor(display, window as u64, cursor as u64);
- }
-
- connection.map_window(window)?;
- connection.flush()?;
-
- let visual = unsafe { x11::xlib::XDefaultVisual(display, screen_num as i32) };
- let colormap = unsafe { x11::xlib::XDefaultColormap(display, screen_num as i32) };
-
- let surface = DrawingSurface::new(
- display,
- window as x11::xlib::Drawable,
- width as u32,
- height as u32,
- visual,
- colormap,
- )?;
-
- let horizontal_padding = (font.height() as f32 * 0.4) as u16;
-
- let tag_widths = config
- .tags
- .iter()
- .map(|tag| {
- let text_width = font.text_width(tag);
- text_width + (horizontal_padding * 2)
- })
- .collect();
-
- let blocks: Vec<Box<dyn Block>> = config
- .status_blocks
- .iter()
- .map(|block_config| block_config.to_block())
- .collect();
-
- let block_underlines: Vec<bool> = config
- .status_blocks
- .iter()
- .map(|block_config| block_config.underline)
- .collect();
-
- let block_last_updates = vec![Instant::now(); blocks.len()];
-
- Ok(Bar {
- window,
- width,
- height,
- graphics_context,
- surface,
- tag_widths,
- needs_redraw: true,
- blocks,
- block_last_updates,
- block_underlines,
- status_text: String::new(),
- tags: config.tags.clone(),
- scheme_normal: config.scheme_normal,
- scheme_occupied: config.scheme_occupied,
- scheme_selected: config.scheme_selected,
- scheme_urgent: config.scheme_urgent,
- hide_vacant_tags: config.hide_vacant_tags,
- last_occupied_tags: 0,
- last_current_tags: 0,
- })
- }
-
- pub fn window(&self) -> Window {
- self.window
- }
-
- pub fn height(&self) -> u16 {
- self.height
- }
-
- pub fn invalidate(&mut self) {
- self.needs_redraw = true;
- }
-
- pub fn update_blocks(&mut self) {
- let now = Instant::now();
- let mut changed = false;
-
- for (i, block) in self.blocks.iter_mut().enumerate() {
- let elapsed = now.duration_since(self.block_last_updates[i]);
-
- if elapsed >= block.interval() && block.content().is_ok() {
- self.block_last_updates[i] = now;
- changed = true;
- }
- }
-
- if changed {
- let mut parts = Vec::new();
- for block in &mut self.blocks {
- if let Ok(text) = block.content() {
- parts.push(text);
- }
- }
- self.status_text = parts.join("");
- self.needs_redraw = true;
- }
- }
-
- pub fn draw(
- &mut self,
- connection: &RustConnection,
- font: &Font,
- display: *mut x11::xlib::Display,
- current_tags: u32,
- occupied_tags: u32,
- urgent_tags: u32,
- draw_blocks: bool,
- layout_symbol: &str,
- keychord_indicator: Option<&str>,
- ) -> Result<(), X11Error> {
- if !self.needs_redraw {
- return Ok(());
- }
-
- connection.change_gc(
- self.graphics_context,
- &ChangeGCAux::new().foreground(self.scheme_normal.background),
- )?;
- connection.flush()?;
-
- unsafe {
- let gc = x11::xlib::XCreateGC(display, self.surface.pixmap(), 0, std::ptr::null_mut());
- x11::xlib::XSetForeground(display, gc, self.scheme_normal.background as u64);
- x11::xlib::XFillRectangle(
- display,
- self.surface.pixmap(),
- gc,
- 0,
- 0,
- self.width as u32,
- self.height as u32,
- );
- x11::xlib::XFreeGC(display, gc);
- }
-
- self.last_occupied_tags = occupied_tags;
- self.last_current_tags = current_tags;
-
- let mut x_position: i16 = 0;
-
- for (tag_index, tag) in self.tags.iter().enumerate() {
- let tag_mask = 1 << tag_index;
- let is_selected = (current_tags & tag_mask) != 0;
- let is_occupied = (occupied_tags & tag_mask) != 0;
- let is_urgent = (urgent_tags & tag_mask) != 0;
-
- if self.hide_vacant_tags && !is_occupied && !is_selected {
- continue;
- }
-
- let tag_width = self.tag_widths[tag_index];
-
- let scheme = if is_selected {
- &self.scheme_selected
- } else if is_urgent {
- &self.scheme_urgent
- } else if is_occupied {
- &self.scheme_occupied
- } else {
- &self.scheme_normal
- };
-
- let text_width = font.text_width(tag);
- let text_x = x_position + ((tag_width - text_width) / 2) as i16;
-
- let top_padding = 4;
- let text_y = top_padding + font.ascent();
-
- self.surface
- .font_draw()
- .draw_text(font, scheme.foreground, text_x, text_y, tag);
-
- if is_selected || is_urgent {
- let font_height = font.height();
- let underline_height = font_height / 8;
- let bottom_gap = 3;
- let underline_y = self.height as i16 - underline_height as i16 - bottom_gap;
-
- let underline_padding = 4;
- let underline_width = tag_width - underline_padding;
- let underline_x = x_position + (underline_padding / 2) as i16;
-
- unsafe {
- let gc = x11::xlib::XCreateGC(
- display,
- self.surface.pixmap(),
- 0,
- std::ptr::null_mut(),
- );
- x11::xlib::XSetForeground(display, gc, scheme.underline as u64);
- x11::xlib::XFillRectangle(
- display,
- self.surface.pixmap(),
- gc,
- underline_x as i32,
- underline_y as i32,
- underline_width as u32,
- underline_height as u32,
- );
- x11::xlib::XFreeGC(display, gc);
- }
- }
-
- x_position += tag_width as i16;
- }
-
- x_position += 10;
-
- let text_x = x_position;
- let top_padding = 4;
- let text_y = top_padding + font.ascent();
-
- self.surface.font_draw().draw_text(
- font,
- self.scheme_normal.foreground,
- text_x,
- text_y,
- layout_symbol,
- );
-
- x_position += font.text_width(layout_symbol) as i16;
-
- if let Some(indicator) = keychord_indicator {
- x_position += 10;
-
- let text_x = x_position;
- let text_y = top_padding + font.ascent();
-
- self.surface.font_draw().draw_text(
- font,
- self.scheme_selected.foreground,
- text_x,
- text_y,
- indicator,
- );
- }
-
- if draw_blocks && !self.status_text.is_empty() {
- let padding = 10;
- let mut x_position = self.width as i16 - padding;
-
- for (i, block) in self.blocks.iter_mut().enumerate().rev() {
- if let Ok(text) = block.content() {
- let text_width = font.text_width(&text);
- x_position -= text_width as i16;
-
- let top_padding = 4;
- let text_y = top_padding + font.ascent();
-
- self.surface.font_draw().draw_text(
- font,
- block.color(),
- x_position,
- text_y,
- &text,
- );
-
- if self.block_underlines[i] {
- let font_height = font.height();
- let underline_height = font_height / 8;
- let bottom_gap = 3;
- let underline_y = self.height as i16 - underline_height as i16 - bottom_gap;
-
- let underline_padding = 8;
- let underline_width = text_width + underline_padding;
- let underline_x = x_position - (underline_padding / 2) as i16;
-
- unsafe {
- let gc = x11::xlib::XCreateGC(
- display,
- self.surface.pixmap(),
- 0,
- std::ptr::null_mut(),
- );
- x11::xlib::XSetForeground(display, gc, block.color() as u64);
- x11::xlib::XFillRectangle(
- display,
- self.surface.pixmap(),
- gc,
- underline_x as i32,
- underline_y as i32,
- underline_width as u32,
- underline_height as u32,
- );
- x11::xlib::XFreeGC(display, gc);
- }
- }
- }
- }
- }
-
- unsafe {
- let gc = x11::xlib::XCreateGC(
- display,
- self.window as x11::xlib::Drawable,
- 0,
- std::ptr::null_mut(),
- );
- x11::xlib::XCopyArea(
- display,
- self.surface.pixmap(),
- self.window as x11::xlib::Drawable,
- gc,
- 0,
- 0,
- self.width as u32,
- self.height as u32,
- 0,
- 0,
- );
- x11::xlib::XFreeGC(display, gc);
- x11::xlib::XSync(display, 1);
- }
-
- self.needs_redraw = false;
-
- Ok(())
- }
-
- pub fn handle_click(&self, click_x: i16) -> Option<usize> {
- let mut current_x_position = 0;
-
- for (tag_index, &tag_width) in self.tag_widths.iter().enumerate() {
- let tag_mask = 1 << tag_index;
- let is_selected = (self.last_current_tags & tag_mask) != 0;
- let is_occupied = (self.last_occupied_tags & tag_mask) != 0;
-
- if self.hide_vacant_tags && !is_occupied && !is_selected {
- continue;
- }
-
- if click_x >= current_x_position && click_x < current_x_position + tag_width as i16 {
- return Some(tag_index);
- }
- current_x_position += tag_width as i16;
- }
- None
- }
-
- pub fn needs_redraw(&self) -> bool {
- self.needs_redraw
- }
-
- pub fn update_from_config(&mut self, config: &Config) {
- self.blocks = config
- .status_blocks
- .iter()
- .map(|block_config| block_config.to_block())
- .collect();
-
- self.block_underlines = config
- .status_blocks
- .iter()
- .map(|block_config| block_config.underline)
- .collect();
-
- self.block_last_updates = vec![Instant::now(); self.blocks.len()];
-
- self.tags = config.tags.clone();
- self.scheme_normal = config.scheme_normal;
- self.scheme_occupied = config.scheme_occupied;
- self.scheme_selected = config.scheme_selected;
- self.scheme_urgent = config.scheme_urgent;
- self.hide_vacant_tags = config.hide_vacant_tags;
-
- self.status_text.clear();
- self.needs_redraw = true;
- }
-}
diff --git a/src/bar/bar.zig b/src/bar/bar.zig
new file mode 100644
index 0000000..6b0e86d
--- /dev/null
+++ b/src/bar/bar.zig
@@ -0,0 +1,359 @@
+const std = @import("std");
+const xlib = @import("../x11/xlib.zig");
+const monitor_mod = @import("../monitor.zig");
+const client_mod = @import("../client.zig");
+const blocks_mod = @import("blocks/blocks.zig");
+const config_mod = @import("../config/config.zig");
+
+const Monitor = monitor_mod.Monitor;
+const Block = blocks_mod.Block;
+
+fn get_layout_symbol(layout_index: u32) []const u8 {
+ const cfg = config_mod.get_config();
+ if (cfg) |conf| {
+ return switch (layout_index) {
+ 0 => conf.layout_tile_symbol,
+ 1 => conf.layout_monocle_symbol,
+ 2 => conf.layout_floating_symbol,
+ 3 => "[S]",
+ else => "[?]",
+ };
+ }
+ return switch (layout_index) {
+ 0 => "[]=",
+ 1 => "[M]",
+ 2 => "><>",
+ 3 => "[S]",
+ else => "[?]",
+ };
+}
+
+pub const ColorScheme = struct {
+ foreground: c_ulong,
+ background: c_ulong,
+ border: c_ulong,
+};
+
+pub const Bar = struct {
+ window: xlib.Window,
+ pixmap: xlib.Pixmap,
+ graphics_context: xlib.GC,
+ xft_draw: ?*xlib.XftDraw,
+ width: i32,
+ height: i32,
+ monitor: *Monitor,
+
+ font: ?*xlib.XftFont,
+ font_height: i32,
+
+ scheme_normal: ColorScheme,
+ scheme_selected: ColorScheme,
+ scheme_occupied: ColorScheme,
+ scheme_urgent: ColorScheme,
+
+ allocator: std.mem.Allocator,
+ blocks: std.ArrayList(Block),
+ needs_redraw: bool,
+ next: ?*Bar,
+
+ pub fn create(
+ allocator: std.mem.Allocator,
+ display: *xlib.Display,
+ screen: c_int,
+ monitor: *Monitor,
+ font_name: []const u8,
+ ) ?*Bar {
+ const bar = allocator.create(Bar) catch return null;
+
+ const visual = xlib.XDefaultVisual(display, screen);
+ const colormap = xlib.XDefaultColormap(display, screen);
+ const depth = xlib.XDefaultDepth(display, screen);
+ const root = xlib.XRootWindow(display, screen);
+
+ const font_name_z = allocator.dupeZ(u8, font_name) catch return null;
+ defer allocator.free(font_name_z);
+
+ const font = xlib.XftFontOpenName(display, screen, font_name_z);
+ if (font == null) {
+ allocator.destroy(bar);
+ return null;
+ }
+
+ const font_height = font.*.ascent + font.*.descent;
+ const bar_height: i32 = @intCast(@as(i32, font_height) + 8);
+
+ const window = xlib.c.XCreateSimpleWindow(
+ display,
+ root,
+ monitor.mon_x,
+ monitor.mon_y,
+ @intCast(monitor.mon_w),
+ @intCast(bar_height),
+ 0,
+ 0,
+ 0x1a1b26,
+ );
+
+ _ = xlib.c.XSetWindowAttributes{};
+ var attributes: xlib.c.XSetWindowAttributes = undefined;
+ attributes.override_redirect = xlib.True;
+ attributes.event_mask = xlib.c.ExposureMask | xlib.c.ButtonPressMask;
+ _ = xlib.c.XChangeWindowAttributes(display, window, xlib.c.CWOverrideRedirect | xlib.c.CWEventMask, &attributes);
+
+ const pixmap = xlib.XCreatePixmap(
+ display,
+ window,
+ @intCast(monitor.mon_w),
+ @intCast(bar_height),
+ @intCast(depth),
+ );
+
+ const graphics_context = xlib.XCreateGC(display, pixmap, 0, null);
+
+ const xft_draw = xlib.XftDrawCreate(display, pixmap, visual, colormap);
+
+ _ = xlib.XMapWindow(display, window);
+
+ const cfg = config_mod.get_config();
+ const scheme_normal = if (cfg) |c| ColorScheme{ .foreground = c.scheme_normal.fg, .background = c.scheme_normal.bg, .border = c.scheme_normal.border } else ColorScheme{ .foreground = 0xbbbbbb, .background = 0x1a1b26, .border = 0x444444 };
+ const scheme_selected = if (cfg) |c| ColorScheme{ .foreground = c.scheme_selected.fg, .background = c.scheme_selected.bg, .border = c.scheme_selected.border } else ColorScheme{ .foreground = 0x0db9d7, .background = 0x1a1b26, .border = 0xad8ee6 };
+ const scheme_occupied = if (cfg) |c| ColorScheme{ .foreground = c.scheme_occupied.fg, .background = c.scheme_occupied.bg, .border = c.scheme_occupied.border } else ColorScheme{ .foreground = 0x0db9d7, .background = 0x1a1b26, .border = 0x0db9d7 };
+ const scheme_urgent = if (cfg) |c| ColorScheme{ .foreground = c.scheme_urgent.fg, .background = c.scheme_urgent.bg, .border = c.scheme_urgent.border } else ColorScheme{ .foreground = 0xf7768e, .background = 0x1a1b26, .border = 0xf7768e };
+
+ bar.* = Bar{
+ .window = window,
+ .pixmap = pixmap,
+ .graphics_context = graphics_context,
+ .xft_draw = xft_draw,
+ .width = monitor.mon_w,
+ .height = bar_height,
+ .monitor = monitor,
+ .font = font,
+ .font_height = font_height,
+ .scheme_normal = scheme_normal,
+ .scheme_selected = scheme_selected,
+ .scheme_occupied = scheme_occupied,
+ .scheme_urgent = scheme_urgent,
+ .allocator = allocator,
+ .blocks = .{},
+ .needs_redraw = true,
+ .next = null,
+ };
+
+ monitor.bar_win = window;
+ monitor.win_y = monitor.mon_y + bar_height;
+ monitor.win_h = monitor.mon_h - bar_height;
+
+ return bar;
+ }
+
+ pub fn destroy(self: *Bar, allocator: std.mem.Allocator, display: *xlib.Display) void {
+ if (self.xft_draw) |xft_draw| {
+ xlib.XftDrawDestroy(xft_draw);
+ }
+ if (self.font) |font| {
+ xlib.XftFontClose(display, font);
+ }
+ _ = xlib.XFreeGC(display, self.graphics_context);
+ _ = xlib.XFreePixmap(display, self.pixmap);
+ _ = xlib.c.XDestroyWindow(display, self.window);
+ self.blocks.deinit(self.allocator);
+ allocator.destroy(self);
+ }
+
+ pub fn add_block(self: *Bar, block: Block) void {
+ self.blocks.append(self.allocator, block) catch {};
+ }
+
+ pub fn invalidate(self: *Bar) void {
+ self.needs_redraw = true;
+ }
+
+ pub fn draw(self: *Bar, display: *xlib.Display, tags: []const []const u8) void {
+ if (!self.needs_redraw) return;
+
+ self.fill_rect(display, 0, 0, self.width, self.height, self.scheme_normal.background);
+
+ var x_position: i32 = 0;
+ const padding: i32 = 8;
+ const monitor = self.monitor;
+ const current_tags = monitor.tagset[monitor.sel_tags];
+
+ for (tags, 0..) |tag, index| {
+ const tag_mask: u32 = @as(u32, 1) << @intCast(index);
+ const is_selected = (current_tags & tag_mask) != 0;
+ const is_occupied = has_clients_on_tag(monitor, tag_mask);
+
+ const scheme = if (is_selected) self.scheme_selected else if (is_occupied) self.scheme_occupied else self.scheme_normal;
+
+ const tag_text_width = self.text_width(display, tag);
+ const tag_width = tag_text_width + padding * 2;
+
+ if (is_selected) {
+ self.fill_rect(display, x_position, self.height - 3, tag_width, 3, scheme.border);
+ }
+
+ const text_y = @divTrunc(self.height + self.font_height, 2) - 4;
+ self.draw_text(display, x_position + padding, text_y, tag, scheme.foreground);
+
+ x_position += tag_width;
+ }
+
+ x_position += padding;
+
+ const layout_symbol = get_layout_symbol(monitor.sel_lt);
+ self.draw_text(display, x_position, @divTrunc(self.height + self.font_height, 2) - 4, layout_symbol, self.scheme_normal.foreground);
+ x_position += self.text_width(display, layout_symbol) + padding;
+
+ var block_x: i32 = self.width - padding;
+ var block_index: usize = self.blocks.items.len;
+ while (block_index > 0) {
+ block_index -= 1;
+ const block = &self.blocks.items[block_index];
+ const content = block.get_content();
+ const content_width = self.text_width(display, content);
+ block_x -= content_width;
+ self.draw_text(display, block_x, @divTrunc(self.height + self.font_height, 2) - 4, content, block.color());
+ if (block.underline) {
+ self.fill_rect(display, block_x, self.height - 2, content_width, 2, block.color());
+ }
+ block_x -= padding;
+ }
+
+ _ = xlib.XCopyArea(display, self.pixmap, self.window, self.graphics_context, 0, 0, @intCast(self.width), @intCast(self.height), 0, 0);
+ _ = xlib.XSync(display, xlib.False);
+
+ self.needs_redraw = false;
+ }
+
+ fn fill_rect(self: *Bar, display: *xlib.Display, x: i32, y: i32, width: i32, height: i32, color: c_ulong) void {
+ _ = xlib.XSetForeground(display, self.graphics_context, color);
+ _ = xlib.XFillRectangle(display, self.pixmap, self.graphics_context, x, y, @intCast(width), @intCast(height));
+ }
+
+ fn draw_text(self: *Bar, display: *xlib.Display, x: i32, y: i32, text: []const u8, color: c_ulong) void {
+ if (self.xft_draw == null or self.font == null) return;
+
+ var xft_color: xlib.XftColor = undefined;
+ var render_color: xlib.XRenderColor = undefined;
+ render_color.red = @intCast((color >> 16 & 0xff) * 257);
+ render_color.green = @intCast((color >> 8 & 0xff) * 257);
+ render_color.blue = @intCast((color & 0xff) * 257);
+ render_color.alpha = 0xffff;
+
+ const visual = xlib.XDefaultVisual(display, 0);
+ const colormap = xlib.XDefaultColormap(display, 0);
+
+ _ = xlib.XftColorAllocValue(display, visual, colormap, &render_color, &xft_color);
+
+ xlib.XftDrawStringUtf8(self.xft_draw, &xft_color, self.font, x, y, text.ptr, @intCast(text.len));
+
+ xlib.XftColorFree(display, visual, colormap, &xft_color);
+ }
+
+ fn text_width(self: *Bar, display: *xlib.Display, text: []const u8) i32 {
+ if (self.font == null) return 0;
+
+ var extents: xlib.XGlyphInfo = undefined;
+ xlib.XftTextExtentsUtf8(display, self.font, text.ptr, @intCast(text.len), &extents);
+ return extents.xOff;
+ }
+
+ pub fn handle_click(self: *Bar, click_x: i32, tags: []const []const u8) ?usize {
+ var x_position: i32 = 0;
+ const padding: i32 = 8;
+ const display = xlib.c.XOpenDisplay(null) orelse return null;
+ defer _ = xlib.XCloseDisplay(display);
+
+ for (tags, 0..) |tag, index| {
+ const tag_text_width = self.text_width(display, tag);
+ const tag_width = tag_text_width + padding * 2;
+
+ if (click_x >= x_position and click_x < x_position + tag_width) {
+ return index;
+ }
+ x_position += tag_width;
+ }
+ return null;
+ }
+
+ pub fn update_blocks(self: *Bar) void {
+ var changed = false;
+ for (self.blocks.items) |*block| {
+ if (block.update()) {
+ changed = true;
+ }
+ }
+ if (changed) {
+ self.needs_redraw = true;
+ }
+ }
+
+ pub fn clear_blocks(self: *Bar) void {
+ self.blocks.clearRetainingCapacity();
+ }
+};
+
+fn has_clients_on_tag(monitor: *Monitor, tag_mask: u32) bool {
+ var current = monitor.clients;
+ while (current) |client| {
+ if ((client.tags & tag_mask) != 0) {
+ return true;
+ }
+ current = client.next;
+ }
+ return false;
+}
+
+pub var bars: ?*Bar = null;
+
+pub fn create_bars(allocator: std.mem.Allocator, display: *xlib.Display, screen: c_int) void {
+ var current_monitor = monitor_mod.monitors;
+ while (current_monitor) |monitor| {
+ const bar = Bar.create(allocator, display, screen, monitor, "monospace:size=10");
+ if (bar) |created_bar| {
+ bars = created_bar;
+ }
+ current_monitor = monitor.next;
+ }
+}
+
+pub fn draw_bars(display: *xlib.Display, tags: []const []const u8) void {
+ var current_monitor = monitor_mod.monitors;
+ while (current_monitor) |monitor| {
+ _ = monitor;
+ if (bars) |bar| {
+ bar.draw(display, tags);
+ }
+ current_monitor = if (current_monitor) |m| m.next else null;
+ }
+}
+
+pub fn invalidate_bars() void {
+ var current = bars;
+ while (current) |bar| {
+ bar.invalidate();
+ current = bar.next;
+ }
+}
+
+pub fn destroy_bars(allocator: std.mem.Allocator, display: *xlib.Display) void {
+ var current = bars;
+ while (current) |bar| {
+ const next = bar.next;
+ bar.destroy(allocator, display);
+ current = next;
+ }
+ bars = null;
+}
+
+pub fn window_to_bar(win: xlib.Window) ?*Bar {
+ var current = bars;
+ while (current) |bar| {
+ if (bar.window == win) {
+ return bar;
+ }
+ current = bar.next;
+ }
+ return null;
+}
diff --git a/src/bar/blocks/battery.rs b/src/bar/blocks/battery.rs
deleted file mode 100644
index b7c4357..0000000
--- a/src/bar/blocks/battery.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use super::Block;
-use crate::errors::BlockError;
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::time::Duration;
-
-pub struct Battery {
- format_charging: String,
- format_discharging: String,
- format_full: String,
- interval: Duration,
- color: u32,
- battery_path: String,
-}
-
-fn detect_battery_name() -> Option<String> {
- let base = Path::new("/sys/class/power_supply");
- let entries = fs::read_dir(base).ok()?;
-
- for entry in entries.flatten() {
- let path: PathBuf = entry.path();
-
- let type_path = path.join("type");
- let present_path = path.join("present");
-
- let is_battery = fs::read_to_string(&type_path)
- .map(|s| s.trim() == "Battery")
- .unwrap_or(false);
-
- let is_present = fs::read_to_string(&present_path)
- .map(|s| s.trim() == "1")
- .unwrap_or(true);
-
- let is_device_scope = fs::read_to_string(path.join("scope"))
- .map(|s| s.trim().eq_ignore_ascii_case("device"))
- .unwrap_or(false);
-
- if !is_battery || !is_present || is_device_scope {
- continue;
- }
-
- if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
- return Some(name.to_string());
- }
- }
- None
-}
-
-impl Battery {
- pub fn new(
- format_charging: &str,
- format_discharging: &str,
- format_full: &str,
- interval_secs: u64,
- color: u32,
- battery_name: Option<String>,
- ) -> Self {
- let name = battery_name
- .or_else(detect_battery_name)
- .unwrap_or_else(|| "BAT0".to_string());
-
- Self {
- format_charging: format_charging.to_string(),
- format_discharging: format_discharging.to_string(),
- format_full: format_full.to_string(),
- interval: Duration::from_secs(interval_secs),
- color,
- battery_path: format!("/sys/class/power_supply/{}", name),
- }
- }
-
- fn read_file(&self, filename: &str) -> Result<String, BlockError> {
- let path = format!("{}/{}", self.battery_path, filename);
- Ok(fs::read_to_string(path)?.trim().to_string())
- }
-
- fn get_capacity(&self) -> Result<u32, BlockError> {
- Ok(self.read_file("capacity")?.parse()?)
- }
-
- fn get_status(&self) -> Result<String, BlockError> {
- self.read_file("status")
- }
-}
-
-impl Block for Battery {
- fn content(&mut self) -> Result<String, BlockError> {
- let capacity = self.get_capacity()?;
- let status = self.get_status()?;
-
- let format = match status.as_str() {
- "Charging" => &self.format_charging,
- "Full" => &self.format_full,
- _ => &self.format_discharging,
- };
-
- Ok(format.replace("{}", &capacity.to_string()))
- }
-
- fn interval(&self) -> Duration {
- self.interval
- }
-
- fn color(&self) -> u32 {
- self.color
- }
-}
diff --git a/src/bar/blocks/battery.zig b/src/bar/blocks/battery.zig
new file mode 100644
index 0000000..812b7d6
--- /dev/null
+++ b/src/bar/blocks/battery.zig
@@ -0,0 +1,87 @@
+const std = @import("std");
+const format_util = @import("format.zig");
+
+pub const Battery = struct {
+ format_charging: []const u8,
+ format_discharging: []const u8,
+ format_full: []const u8,
+ battery_name: []const u8,
+ interval_secs: u64,
+ color: c_ulong,
+
+ pub fn init(
+ format_charging: []const u8,
+ format_discharging: []const u8,
+ format_full: []const u8,
+ battery_name: []const u8,
+ interval_secs: u64,
+ color: c_ulong,
+ ) Battery {
+ return .{
+ .format_charging = format_charging,
+ .format_discharging = format_discharging,
+ .format_full = format_full,
+ .battery_name = if (battery_name.len > 0) battery_name else "BAT0",
+ .interval_secs = interval_secs,
+ .color = color,
+ };
+ }
+
+ pub fn content(self: *Battery, buffer: []u8) []const u8 {
+ var path_buf: [128]u8 = undefined;
+
+ const capacity = self.read_battery_file(&path_buf, "capacity") orelse return buffer[0..0];
+ const status = self.read_battery_status(&path_buf) orelse return buffer[0..0];
+
+ const format = switch (status) {
+ .charging => self.format_charging,
+ .discharging => self.format_discharging,
+ .full => self.format_full,
+ };
+
+ var cap_buf: [8]u8 = undefined;
+ const cap_str = std.fmt.bufPrint(&cap_buf, "{d}", .{capacity}) catch return buffer[0..0];
+
+ return format_util.substitute(format, cap_str, buffer);
+ }
+
+ const Status = enum { charging, discharging, full };
+
+ fn read_battery_status(self: *Battery, path_buf: *[128]u8) ?Status {
+ const path = std.fmt.bufPrint(path_buf, "/sys/class/power_supply/{s}/status", .{self.battery_name}) catch return null;
+
+ const file = std.fs.openFileAbsolute(path, .{}) catch return null;
+ defer file.close();
+
+ var buf: [32]u8 = undefined;
+ const len = file.read(&buf) catch return null;
+ const status_str = std.mem.trim(u8, buf[0..len], " \n\r\t");
+
+ if (std.mem.eql(u8, status_str, "Charging")) return .charging;
+ if (std.mem.eql(u8, status_str, "Discharging")) return .discharging;
+ if (std.mem.eql(u8, status_str, "Full")) return .full;
+ if (std.mem.eql(u8, status_str, "Not charging")) return .full;
+ return .discharging;
+ }
+
+ fn read_battery_file(self: *Battery, path_buf: *[128]u8, file_name: []const u8) ?u8 {
+ const path = std.fmt.bufPrint(path_buf, "/sys/class/power_supply/{s}/{s}", .{ self.battery_name, file_name }) catch return null;
+
+ const file = std.fs.openFileAbsolute(path, .{}) catch return null;
+ defer file.close();
+
+ var buf: [16]u8 = undefined;
+ const len = file.read(&buf) catch return null;
+ const value_str = std.mem.trim(u8, buf[0..len], " \n\r\t");
+
+ return std.fmt.parseInt(u8, value_str, 10) catch null;
+ }
+
+ pub fn interval(self: *Battery) u64 {
+ return self.interval_secs;
+ }
+
+ pub fn get_color(self: *Battery) c_ulong {
+ return self.color;
+ }
+};
diff --git a/src/bar/blocks/blocks.zig b/src/bar/blocks/blocks.zig
new file mode 100644
index 0000000..861bcd3
--- /dev/null
+++ b/src/bar/blocks/blocks.zig
@@ -0,0 +1,161 @@
+const std = @import("std");
+
+pub const Static = @import("static.zig").Static;
+pub const Date_Time = @import("datetime.zig").Date_Time;
+pub const Ram = @import("ram.zig").Ram;
+pub const Shell = @import("shell.zig").Shell;
+pub const Battery = @import("battery.zig").Battery;
+pub const Cpu_Temp = @import("cpu_temp.zig").Cpu_Temp;
+
+pub const Block_Type = enum {
+ static,
+ datetime,
+ ram,
+ shell,
+ battery,
+ cpu_temp,
+};
+
+pub const Block = struct {
+ data: Data,
+ last_update: i64,
+ cached_content: [256]u8,
+ cached_len: usize,
+ underline: bool,
+
+ pub const Data = union(Block_Type) {
+ static: Static,
+ datetime: Date_Time,
+ ram: Ram,
+ shell: Shell,
+ battery: Battery,
+ cpu_temp: Cpu_Temp,
+ };
+
+ pub fn init_static(text: []const u8, col: c_ulong, ul: bool) Block {
+ var block = Block{
+ .data = .{ .static = Static.init(text, col) },
+ .last_update = 0,
+ .cached_content = undefined,
+ .cached_len = 0,
+ .underline = ul,
+ };
+ @memcpy(block.cached_content[0..text.len], text);
+ block.cached_len = text.len;
+ return block;
+ }
+
+ pub fn init_datetime(format: []const u8, datetime_format: []const u8, interval_secs: u64, col: c_ulong, ul: bool) Block {
+ return .{
+ .data = .{ .datetime = Date_Time.init(format, datetime_format, interval_secs, col) },
+ .last_update = 0,
+ .cached_content = undefined,
+ .cached_len = 0,
+ .underline = ul,
+ };
+ }
+
+ pub fn init_ram(format: []const u8, interval_secs: u64, col: c_ulong, ul: bool) Block {
+ return .{
+ .data = .{ .ram = Ram.init(format, interval_secs, col) },
+ .last_update = 0,
+ .cached_content = undefined,
+ .cached_len = 0,
+ .underline = ul,
+ };
+ }
+
+ pub fn init_shell(format: []const u8, command: []const u8, interval_secs: u64, col: c_ulong, ul: bool) Block {
+ return .{
+ .data = .{ .shell = Shell.init(format, command, interval_secs, col) },
+ .last_update = 0,
+ .cached_content = undefined,
+ .cached_len = 0,
+ .underline = ul,
+ };
+ }
+
+ pub fn init_battery(
+ format_charging: []const u8,
+ format_discharging: []const u8,
+ format_full: []const u8,
+ battery_name: []const u8,
+ interval_secs: u64,
+ col: c_ulong,
+ ul: bool,
+ ) Block {
+ return .{
+ .data = .{ .battery = Battery.init(format_charging, format_discharging, format_full, battery_name, interval_secs, col) },
+ .last_update = 0,
+ .cached_content = undefined,
+ .cached_len = 0,
+ .underline = ul,
+ };
+ }
+
+ pub fn init_cpu_temp(
+ format: []const u8,
+ thermal_zone: []const u8,
+ interval_secs: u64,
+ col: c_ulong,
+ ul: bool,
+ ) Block {
+ return .{
+ .data = .{ .cpu_temp = Cpu_Temp.init(format, thermal_zone, interval_secs, col) },
+ .last_update = 0,
+ .cached_content = undefined,
+ .cached_len = 0,
+ .underline = ul,
+ };
+ }
+
+ pub fn update(self: *Block) bool {
+ const interval_secs = self.interval();
+ if (interval_secs == 0) return false;
+
+ const now = std.time.timestamp();
+ if (now - self.last_update < @as(i64, @intCast(interval_secs))) {
+ return false;
+ }
+
+ self.last_update = now;
+
+ const result = switch (self.data) {
+ .static => |*s| s.content(&self.cached_content),
+ .datetime => |*d| d.content(&self.cached_content),
+ .ram => |*r| r.content(&self.cached_content),
+ .shell => |*s| s.content(&self.cached_content),
+ .battery => |*b| b.content(&self.cached_content),
+ .cpu_temp => |*c| c.content(&self.cached_content),
+ };
+
+ self.cached_len = result.len;
+ return true;
+ }
+
+ pub fn interval(self: *Block) u64 {
+ return switch (self.data) {
+ .static => |*s| s.interval(),
+ .datetime => |*d| d.interval(),
+ .ram => |*r| r.interval(),
+ .shell => |*s| s.interval(),
+ .battery => |*b| b.interval(),
+ .cpu_temp => |*c| c.interval(),
+ };
+ }
+
+ pub fn color(self: *const Block) c_ulong {
+ return switch (self.data) {
+ .static => |s| s.color,
+ .datetime => |d| d.color,
+ .ram => |r| r.color,
+ .shell => |s| s.color,
+ .battery => |b| b.color,
+ .cpu_temp => |c| c.color,
+ };
+ }
+
+ pub fn get_content(self: *const Block) []const u8 {
+ return self.cached_content[0..self.cached_len];
+ }
+};
diff --git a/src/bar/blocks/cpu_temp.zig b/src/bar/blocks/cpu_temp.zig
new file mode 100644
index 0000000..539e49b
--- /dev/null
+++ b/src/bar/blocks/cpu_temp.zig
@@ -0,0 +1,113 @@
+const std = @import("std");
+const format_util = @import("format.zig");
+
+pub const Cpu_Temp = struct {
+ format: []const u8,
+ device: []const u8,
+ interval_secs: u64,
+ color: c_ulong,
+ cached_path: [128]u8,
+ cached_path_len: usize,
+
+ pub fn init(
+ format: []const u8,
+ device: []const u8,
+ interval_secs: u64,
+ color: c_ulong,
+ ) Cpu_Temp {
+ var self = Cpu_Temp{
+ .format = format,
+ .device = device,
+ .interval_secs = interval_secs,
+ .color = color,
+ .cached_path = undefined,
+ .cached_path_len = 0,
+ };
+ self.detect_path();
+ return self;
+ }
+
+ fn detect_path(self: *Cpu_Temp) void {
+ if (self.device.len > 0 and self.device[0] == '/') {
+ if (self.device.len <= self.cached_path.len) {
+ @memcpy(self.cached_path[0..self.device.len], self.device);
+ self.cached_path_len = self.device.len;
+ }
+ return;
+ }
+
+ if (self.device.len > 0) {
+ if (self.try_path("/sys/class/thermal/{s}/temp", self.device)) return;
+ if (self.try_path("/sys/class/hwmon/{s}/temp1_input", self.device)) return;
+ }
+
+ if (self.find_hwmon_cpu()) return;
+
+ if (self.try_path("/sys/class/thermal/{s}/temp", "thermal_zone0")) return;
+ }
+
+ fn try_path(self: *Cpu_Temp, comptime fmt: []const u8, device: []const u8) bool {
+ const path = std.fmt.bufPrint(&self.cached_path, fmt, .{device}) catch return false;
+ const file = std.fs.openFileAbsolute(path, .{}) catch return false;
+ file.close();
+ self.cached_path_len = path.len;
+ return true;
+ }
+
+ fn find_hwmon_cpu(self: *Cpu_Temp) bool {
+ var dir = std.fs.openDirAbsolute("/sys/class/hwmon", .{ .iterate = true }) catch return false;
+ defer dir.close();
+
+ var iter = dir.iterate();
+ while (iter.next() catch null) |entry| {
+ if (entry.kind != .directory and entry.kind != .sym_link) continue;
+
+ var name_path: [128]u8 = undefined;
+ const name_path_str = std.fmt.bufPrint(&name_path, "/sys/class/hwmon/{s}/name", .{entry.name}) catch continue;
+
+ const name_file = std.fs.openFileAbsolute(name_path_str, .{}) catch continue;
+ defer name_file.close();
+
+ var name_buf: [32]u8 = undefined;
+ const name_len = name_file.read(&name_buf) catch continue;
+ const name = std.mem.trim(u8, name_buf[0..name_len], " \n\r\t");
+
+ if (std.mem.eql(u8, name, "coretemp") or std.mem.eql(u8, name, "k10temp")) {
+ const temp_path = std.fmt.bufPrint(&self.cached_path, "/sys/class/hwmon/{s}/temp1_input", .{entry.name}) catch continue;
+ const temp_file = std.fs.openFileAbsolute(temp_path, .{}) catch continue;
+ temp_file.close();
+ self.cached_path_len = temp_path.len;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ pub fn content(self: *Cpu_Temp, buffer: []u8) []const u8 {
+ if (self.cached_path_len == 0) return buffer[0..0];
+
+ const path = self.cached_path[0..self.cached_path_len];
+ const file = std.fs.openFileAbsolute(path, .{}) catch return buffer[0..0];
+ defer file.close();
+
+ var temp_buf: [16]u8 = undefined;
+ const len = file.read(&temp_buf) catch return buffer[0..0];
+ const temp_str = std.mem.trim(u8, temp_buf[0..len], " \n\r\t");
+
+ const millidegrees = std.fmt.parseInt(i32, temp_str, 10) catch return buffer[0..0];
+ const degrees = @divTrunc(millidegrees, 1000);
+
+ var deg_buf: [8]u8 = undefined;
+ const deg_str = std.fmt.bufPrint(°_buf, "{d}", .{degrees}) catch return buffer[0..0];
+
+ return format_util.substitute(self.format, deg_str, buffer);
+ }
+
+ pub fn interval(self: *Cpu_Temp) u64 {
+ return self.interval_secs;
+ }
+
+ pub fn get_color(self: *Cpu_Temp) c_ulong {
+ return self.color;
+ }
+};
diff --git a/src/bar/blocks/datetime.rs b/src/bar/blocks/datetime.rs
deleted file mode 100644
index 9285176..0000000
--- a/src/bar/blocks/datetime.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use super::Block;
-use crate::errors::BlockError;
-use chrono::Local;
-use std::time::Duration;
-
-pub struct DateTime {
- format_template: String,
- time_format: String,
- interval: Duration,
- color: u32,
-}
-
-impl DateTime {
- pub fn new(format_template: &str, time_format: &str, interval_secs: u64, color: u32) -> Self {
- Self {
- format_template: format_template.to_string(),
- time_format: time_format.to_string(),
- interval: Duration::from_secs(interval_secs),
- color,
- }
- }
-}
-
-impl Block for DateTime {
- fn content(&mut self) -> Result<String, BlockError> {
- let now = Local::now();
- let time_str = now.format(&self.time_format).to_string();
- Ok(self.format_template.replace("{}", &time_str))
- }
-
- fn interval(&self) -> Duration {
- self.interval
- }
-
- fn color(&self) -> u32 {
- self.color
- }
-}
diff --git a/src/bar/blocks/datetime.zig b/src/bar/blocks/datetime.zig
new file mode 100644
index 0000000..e176385
--- /dev/null
+++ b/src/bar/blocks/datetime.zig
@@ -0,0 +1,107 @@
+const std = @import("std");
+const format_util = @import("format.zig");
+const c = @cImport({
+ @cInclude("time.h");
+});
+
+pub const Date_Time = struct {
+ format: []const u8,
+ datetime_format: []const u8,
+ interval_secs: u64,
+ color: c_ulong,
+
+ pub fn init(format: []const u8, datetime_format: []const u8, interval_secs: u64, color: c_ulong) Date_Time {
+ return .{
+ .format = format,
+ .datetime_format = datetime_format,
+ .interval_secs = interval_secs,
+ .color = color,
+ };
+ }
+
+ pub fn content(self: *Date_Time, buffer: []u8) []const u8 {
+ var now: c.time_t = c.time(null);
+ const tm_ptr = c.localtime(&now);
+ if (tm_ptr == null) return buffer[0..0];
+ const tm = tm_ptr.*;
+
+ const hours: u32 = @intCast(tm.tm_hour);
+ const minutes: u32 = @intCast(tm.tm_min);
+ const seconds: u32 = @intCast(tm.tm_sec);
+ const day: u8 = @intCast(tm.tm_mday);
+ const month: u8 = @intCast(tm.tm_mon + 1);
+ const year: i32 = tm.tm_year + 1900;
+ const dow: i32 = tm.tm_wday;
+
+ var datetime_buf: [64]u8 = undefined;
+ var dt_len: usize = 0;
+
+ var fmt_idx: usize = 0;
+ while (fmt_idx < self.datetime_format.len and dt_len < datetime_buf.len - 10) {
+ if (self.datetime_format[fmt_idx] == '%' and fmt_idx + 1 < self.datetime_format.len) {
+ const next = self.datetime_format[fmt_idx + 1];
+ if (next == '-' and fmt_idx + 2 < self.datetime_format.len) {
+ const spec = self.datetime_format[fmt_idx + 2];
+ dt_len += format_spec(spec, false, hours, minutes, seconds, day, month, year, dow, datetime_buf[dt_len..]);
+ fmt_idx += 3;
+ } else {
+ dt_len += format_spec(next, true, hours, minutes, seconds, day, month, year, dow, datetime_buf[dt_len..]);
+ fmt_idx += 2;
+ }
+ } else {
+ datetime_buf[dt_len] = self.datetime_format[fmt_idx];
+ dt_len += 1;
+ fmt_idx += 1;
+ }
+ }
+
+ return format_util.substitute(self.format, datetime_buf[0..dt_len], buffer);
+ }
+
+ fn format_spec(spec: u8, pad: bool, hours: u32, minutes: u32, seconds: u32, day: u8, month: u8, year: i32, dow: i32, buf: []u8) usize {
+ const day_names = [_][]const u8{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+ const month_names = [_][]const u8{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+
+ return switch (spec) {
+ 'Y' => (std.fmt.bufPrint(buf, "{d}", .{year}) catch return 0).len,
+ 'm' => if (pad) (std.fmt.bufPrint(buf, "{d:0>2}", .{month}) catch return 0).len else (std.fmt.bufPrint(buf, "{d}", .{month}) catch return 0).len,
+ 'd' => if (pad) (std.fmt.bufPrint(buf, "{d:0>2}", .{day}) catch return 0).len else (std.fmt.bufPrint(buf, "{d}", .{day}) catch return 0).len,
+ 'H' => if (pad) (std.fmt.bufPrint(buf, "{d:0>2}", .{hours}) catch return 0).len else (std.fmt.bufPrint(buf, "{d}", .{hours}) catch return 0).len,
+ 'M' => if (pad) (std.fmt.bufPrint(buf, "{d:0>2}", .{minutes}) catch return 0).len else (std.fmt.bufPrint(buf, "{d}", .{minutes}) catch return 0).len,
+ 'S' => if (pad) (std.fmt.bufPrint(buf, "{d:0>2}", .{seconds}) catch return 0).len else (std.fmt.bufPrint(buf, "{d}", .{seconds}) catch return 0).len,
+ 'I' => blk: {
+ const h12 = if (hours == 0) 12 else if (hours > 12) hours - 12 else hours;
+ break :blk if (pad) (std.fmt.bufPrint(buf, "{d:0>2}", .{h12}) catch return 0).len else (std.fmt.bufPrint(buf, "{d}", .{h12}) catch return 0).len;
+ },
+ 'p' => blk: {
+ const s: []const u8 = if (hours >= 12) "PM" else "AM";
+ @memcpy(buf[0..2], s);
+ break :blk 2;
+ },
+ 'P' => blk: {
+ const s: []const u8 = if (hours >= 12) "pm" else "am";
+ @memcpy(buf[0..2], s);
+ break :blk 2;
+ },
+ 'a' => blk: {
+ const name = day_names[@intCast(dow)];
+ @memcpy(buf[0..3], name);
+ break :blk 3;
+ },
+ 'b' => blk: {
+ const name = month_names[month - 1];
+ @memcpy(buf[0..3], name);
+ break :blk 3;
+ },
+ else => 0,
+ };
+ }
+
+ pub fn interval(self: *Date_Time) u64 {
+ return self.interval_secs;
+ }
+
+ pub fn get_color(self: *Date_Time) c_ulong {
+ return self.color;
+ }
+};
diff --git a/src/bar/blocks/format.zig b/src/bar/blocks/format.zig
new file mode 100644
index 0000000..f3c90ef
--- /dev/null
+++ b/src/bar/blocks/format.zig
@@ -0,0 +1,52 @@
+pub fn substitute(format: []const u8, value: []const u8, buffer: []u8) []const u8 {
+ if (format.len == 0) {
+ const len = @min(value.len, buffer.len);
+ @memcpy(buffer[0..len], value[0..len]);
+ return buffer[0..len];
+ }
+
+ var out_idx: usize = 0;
+ var fmt_idx: usize = 0;
+ while (fmt_idx < format.len and out_idx < buffer.len -| value.len) {
+ if (fmt_idx + 1 < format.len and format[fmt_idx] == '{' and format[fmt_idx + 1] == '}') {
+ @memcpy(buffer[out_idx .. out_idx + value.len], value);
+ out_idx += value.len;
+ fmt_idx += 2;
+ } else {
+ buffer[out_idx] = format[fmt_idx];
+ out_idx += 1;
+ fmt_idx += 1;
+ }
+ }
+ return buffer[0..out_idx];
+}
+
+pub fn substitute_multi(format: []const u8, values: []const []const u8, buffer: []u8) []const u8 {
+ if (format.len == 0 and values.len > 0) {
+ const len = @min(values[0].len, buffer.len);
+ @memcpy(buffer[0..len], values[0][0..len]);
+ return buffer[0..len];
+ }
+
+ var out_idx: usize = 0;
+ var fmt_idx: usize = 0;
+ var val_idx: usize = 0;
+ while (fmt_idx < format.len and out_idx < buffer.len - 20) {
+ if (fmt_idx + 1 < format.len and format[fmt_idx] == '{' and format[fmt_idx + 1] == '}') {
+ if (val_idx < values.len) {
+ const value = values[val_idx];
+ if (out_idx + value.len <= buffer.len) {
+ @memcpy(buffer[out_idx .. out_idx + value.len], value);
+ out_idx += value.len;
+ }
+ val_idx += 1;
+ }
+ fmt_idx += 2;
+ } else {
+ buffer[out_idx] = format[fmt_idx];
+ out_idx += 1;
+ fmt_idx += 1;
+ }
+ }
+ return buffer[0..out_idx];
+}
diff --git a/src/bar/blocks/mod.rs b/src/bar/blocks/mod.rs
deleted file mode 100644
index 5c090f2..0000000
--- a/src/bar/blocks/mod.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-use crate::errors::BlockError;
-use std::time::Duration;
-
-mod battery;
-mod datetime;
-mod ram;
-mod shell;
-
-use battery::Battery;
-use datetime::DateTime;
-use ram::Ram;
-use shell::ShellBlock;
-
-pub trait Block {
- fn content(&mut self) -> Result<String, BlockError>;
- fn interval(&self) -> Duration;
- fn color(&self) -> u32;
-}
-
-#[derive(Debug, Clone)]
-pub struct BlockConfig {
- pub format: String,
- pub command: BlockCommand,
- pub interval_secs: u64,
- pub color: u32,
- pub underline: bool,
-}
-
-#[derive(Debug, Clone)]
-pub enum BlockCommand {
- Shell(String),
- DateTime(String),
- Battery {
- format_charging: String,
- format_discharging: String,
- format_full: String,
- battery_name: Option<String>,
- },
- Ram,
- Static(String),
-}
-
-impl BlockConfig {
- pub fn to_block(&self) -> Box<dyn Block> {
- match &self.command {
- BlockCommand::Shell(cmd) => Box::new(ShellBlock::new(
- &self.format,
- cmd,
- self.interval_secs,
- self.color,
- )),
- BlockCommand::DateTime(fmt) => Box::new(DateTime::new(
- &self.format,
- fmt,
- self.interval_secs,
- self.color,
- )),
- BlockCommand::Battery {
- format_charging,
- format_discharging,
- format_full,
- battery_name,
- } => Box::new(Battery::new(
- format_charging,
- format_discharging,
- format_full,
- self.interval_secs,
- self.color,
- battery_name.clone(),
- )),
- BlockCommand::Ram => Box::new(Ram::new(&self.format, self.interval_secs, self.color)),
- BlockCommand::Static(text) => Box::new(StaticBlock::new(
- &format!("{}{}", self.format, text),
- self.color,
- )),
- }
- }
-}
-
-struct StaticBlock {
- text: String,
- color: u32,
-}
-
-impl StaticBlock {
- fn new(text: &str, color: u32) -> Self {
- Self {
- text: text.to_string(),
- color,
- }
- }
-}
-
-impl Block for StaticBlock {
- fn content(&mut self) -> Result<String, BlockError> {
- Ok(self.text.clone())
- }
-
- fn interval(&self) -> Duration {
- Duration::from_secs(u64::MAX)
- }
-
- fn color(&self) -> u32 {
- self.color
- }
-}
diff --git a/src/bar/blocks/ram.rs b/src/bar/blocks/ram.rs
deleted file mode 100644
index c26571a..0000000
--- a/src/bar/blocks/ram.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-use super::Block;
-use crate::errors::BlockError;
-use std::fs;
-use std::time::Duration;
-
-pub struct Ram {
- format: String,
- interval: Duration,
- color: u32,
-}
-
-impl Ram {
- pub fn new(format: &str, interval_secs: u64, color: u32) -> Self {
- Self {
- format: format.to_string(),
- interval: Duration::from_secs(interval_secs),
- color,
- }
- }
-
- fn get_memory_info(&self) -> Result<(u64, u64, f32), BlockError> {
- let meminfo = fs::read_to_string("/proc/meminfo")?;
- let mut total: u64 = 0;
- let mut available: u64 = 0;
-
- for line in meminfo.lines() {
- if line.starts_with("MemTotal:") {
- total = line
- .split_whitespace()
- .nth(1)
- .and_then(|s| s.parse().ok())
- .unwrap_or(0)
- } else if line.starts_with("MemAvailable:") {
- available = line
- .split_whitespace()
- .nth(1)
- .and_then(|s| s.parse().ok())
- .unwrap_or(0)
- }
- }
-
- let used = total.saturating_sub(available);
- let percentage = if total > 0 {
- (used as f32 / total as f32) * 100.0
- } else {
- 0.0
- };
-
- Ok((used, total, percentage))
- }
-}
-
-impl Block for Ram {
- fn content(&mut self) -> Result<String, BlockError> {
- let (used, total, percentage) = self.get_memory_info()?;
-
- let used_gb = used as f32 / 1024.0 / 1024.0;
- let total_gb = total as f32 / 1024.0 / 1024.0;
-
- let result = self
- .format
- .replace("{used}", &format!("{:.1}", used_gb))
- .replace("{total}", &format!("{:.1}", total_gb))
- .replace("{percent}", &format!("{:.1}", percentage))
- .replace("{}", &format!("{:.1}", used_gb));
-
- Ok(result)
- }
-
- fn interval(&self) -> Duration {
- self.interval
- }
-
- fn color(&self) -> u32 {
- self.color
- }
-}
diff --git a/src/bar/blocks/ram.zig b/src/bar/blocks/ram.zig
new file mode 100644
index 0000000..55ba924
--- /dev/null
+++ b/src/bar/blocks/ram.zig
@@ -0,0 +1,111 @@
+const std = @import("std");
+
+pub const Ram = struct {
+ format: []const u8,
+ interval_secs: u64,
+ color: c_ulong,
+
+ pub fn init(format: []const u8, interval_secs: u64, color: c_ulong) Ram {
+ return .{
+ .format = format,
+ .interval_secs = interval_secs,
+ .color = color,
+ };
+ }
+
+ pub fn content(self: *Ram, buffer: []u8) []const u8 {
+ const file = std.fs.openFileAbsolute("/proc/meminfo", .{}) catch return buffer[0..0];
+ defer file.close();
+
+ var read_buffer: [512]u8 = undefined;
+ const bytes_read = file.read(&read_buffer) catch return buffer[0..0];
+ const file_content = read_buffer[0..bytes_read];
+
+ var total: u64 = 0;
+ var available: u64 = 0;
+
+ var lines = std.mem.splitScalar(u8, file_content, '\n');
+ while (lines.next()) |line| {
+ if (std.mem.startsWith(u8, line, "MemTotal:")) {
+ total = parse_mem_value(line);
+ } else if (std.mem.startsWith(u8, line, "MemAvailable:")) {
+ available = parse_mem_value(line);
+ }
+ }
+
+ if (total == 0) return buffer[0..0];
+
+ const used = total - available;
+ const used_gb = @as(f32, @floatFromInt(used)) / 1024.0 / 1024.0;
+ const total_gb = @as(f32, @floatFromInt(total)) / 1024.0 / 1024.0;
+ const percent = (@as(f32, @floatFromInt(used)) / @as(f32, @floatFromInt(total))) * 100.0;
+
+ var val_bufs: [3][16]u8 = undefined;
+ const used_str = std.fmt.bufPrint(&val_bufs[0], "{d:.1}", .{used_gb}) catch return buffer[0..0];
+ const total_str = std.fmt.bufPrint(&val_bufs[1], "{d:.1}", .{total_gb}) catch return buffer[0..0];
+ const percent_str = std.fmt.bufPrint(&val_bufs[2], "{d:.1}", .{percent}) catch return buffer[0..0];
+
+ return substitute(self.format, buffer, .{
+ .used = used_str,
+ .total = total_str,
+ .percent = percent_str,
+ });
+ }
+
+ pub fn interval(self: *Ram) u64 {
+ return self.interval_secs;
+ }
+
+ pub fn get_color(self: *Ram) c_ulong {
+ return self.color;
+ }
+};
+
+fn parse_mem_value(line: []const u8) u64 {
+ var iter = std.mem.tokenizeAny(u8, line, ": \tkB");
+ _ = iter.next();
+ if (iter.next()) |value_str| {
+ return std.fmt.parseInt(u64, value_str, 10) catch 0;
+ }
+ return 0;
+}
+
+fn substitute(format: []const u8, buffer: []u8, values: struct {
+ used: []const u8,
+ total: []const u8,
+ percent: []const u8,
+}) []const u8 {
+ var pos: usize = 0;
+ var i: usize = 0;
+
+ while (i < format.len) {
+ if (format[i] == '{' and i + 1 < format.len) {
+ const rest = format[i..];
+ const repl = if (std.mem.startsWith(u8, rest, "{used}")) blk: {
+ i += 6;
+ break :blk values.used;
+ } else if (std.mem.startsWith(u8, rest, "{total}")) blk: {
+ i += 7;
+ break :blk values.total;
+ } else if (std.mem.startsWith(u8, rest, "{percent}")) blk: {
+ i += 9;
+ break :blk values.percent;
+ } else if (std.mem.startsWith(u8, rest, "{}")) blk: {
+ i += 2;
+ break :blk values.used;
+ } else null;
+
+ if (repl) |r| {
+ if (pos + r.len > buffer.len) break;
+ @memcpy(buffer[pos..][0..r.len], r);
+ pos += r.len;
+ continue;
+ }
+ }
+ if (pos >= buffer.len) break;
+ buffer[pos] = format[i];
+ pos += 1;
+ i += 1;
+ }
+ return buffer[0..pos];
+}
diff --git a/src/bar/blocks/shell.rs b/src/bar/blocks/shell.rs
deleted file mode 100644
index 81930e5..0000000
--- a/src/bar/blocks/shell.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-use super::Block;
-use crate::errors::BlockError;
-use std::process::Command;
-use std::time::{Duration, Instant};
-
-pub struct ShellBlock {
- format: String,
- command: String,
- interval: Duration,
- color: u32,
- cached_output: Option<String>,
- last_run: Option<Instant>,
-}
-
-impl ShellBlock {
- pub fn new(format: &str, command: &str, interval_secs: u64, color: u32) -> Self {
- Self {
- format: format.to_string(),
- command: command.to_string(),
- interval: Duration::from_secs(interval_secs),
- color,
- cached_output: None,
- last_run: None,
- }
- }
-
- fn execute(&mut self) -> Result<String, BlockError> {
- let output = Command::new("sh")
- .arg("-c")
- .arg(&self.command)
- .output()
- .map_err(|e| BlockError::CommandFailed(format!("Failed to execute command: {}", e)))?;
-
- if !output.status.success() {
- return Err(BlockError::CommandFailed(format!(
- "Command exited with status: {}",
- output.status
- )));
- }
-
- let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
- let formatted = self.format.replace("{}", &result);
-
- self.cached_output = Some(formatted.clone());
- self.last_run = Some(Instant::now());
-
- Ok(formatted)
- }
-}
-
-impl Block for ShellBlock {
- fn content(&mut self) -> Result<String, BlockError> {
- let should_refresh = match self.last_run {
- None => true,
- Some(last) => last.elapsed() >= self.interval,
- };
-
- if should_refresh {
- return self.execute();
- }
-
- self.cached_output
- .clone()
- .ok_or_else(|| BlockError::CommandFailed("No cached output".to_string()))
- }
-
- fn interval(&self) -> Duration {
- self.interval
- }
-
- fn color(&self) -> u32 {
- self.color
- }
-}
diff --git a/src/bar/blocks/shell.zig b/src/bar/blocks/shell.zig
new file mode 100644
index 0000000..b884160
--- /dev/null
+++ b/src/bar/blocks/shell.zig
@@ -0,0 +1,45 @@
+const std = @import("std");
+const format_util = @import("format.zig");
+
+pub const Shell = struct {
+ format: []const u8,
+ command: []const u8,
+ interval_secs: u64,
+ color: c_ulong,
+
+ pub fn init(format: []const u8, command: []const u8, interval_secs: u64, col: c_ulong) Shell {
+ return .{
+ .format = format,
+ .command = command,
+ .interval_secs = interval_secs,
+ .color = col,
+ };
+ }
+
+ pub fn content(self: *Shell, buffer: []u8) []const u8 {
+ var cmd_output: [256]u8 = undefined;
+ const result = std.process.Child.run(.{
+ .allocator = std.heap.page_allocator,
+ .argv = &.{ "/bin/sh", "-c", self.command },
+ }) catch return buffer[0..0];
+ defer std.heap.page_allocator.free(result.stdout);
+ defer std.heap.page_allocator.free(result.stderr);
+
+ var cmd_len = @min(result.stdout.len, cmd_output.len);
+ @memcpy(cmd_output[0..cmd_len], result.stdout[0..cmd_len]);
+
+ while (cmd_len > 0 and (cmd_output[cmd_len - 1] == '\n' or cmd_output[cmd_len - 1] == '\r')) {
+ cmd_len -= 1;
+ }
+
+ return format_util.substitute(self.format, cmd_output[0..cmd_len], buffer);
+ }
+
+ pub fn interval(self: *Shell) u64 {
+ return self.interval_secs;
+ }
+
+ pub fn get_color(self: *Shell) c_ulong {
+ return self.color;
+ }
+};
diff --git a/src/bar/blocks/static.zig b/src/bar/blocks/static.zig
new file mode 100644
index 0000000..d243cb5
--- /dev/null
+++ b/src/bar/blocks/static.zig
@@ -0,0 +1,24 @@
+const std = @import("std");
+
+pub const Static = struct {
+ text: []const u8,
+ color: c_ulong,
+
+ pub fn init(text: []const u8, color: c_ulong) Static {
+ return .{ .text = text, .color = color };
+ }
+
+ pub fn content(self: *Static, buffer: []u8) []const u8 {
+ const len = @min(self.text.len, buffer.len);
+ @memcpy(buffer[0..len], self.text[0..len]);
+ return buffer[0..len];
+ }
+
+ pub fn interval(_: *Static) u64 {
+ return 0;
+ }
+
+ pub fn get_color(self: *Static) c_ulong {
+ return self.color;
+ }
+};
diff --git a/src/bar/font.rs b/src/bar/font.rs
deleted file mode 100644
index 9d45ae4..0000000
--- a/src/bar/font.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-use std::ffi::CString;
-use x11::xft::{XftColor, XftDraw, XftDrawStringUtf8, XftFont, XftFontOpenName};
-use x11::xlib::{Colormap, Display, Drawable, Visual};
-use x11::xrender::XRenderColor;
-
-use crate::errors::X11Error;
-
-pub struct Font {
- xft_font: *mut XftFont,
- display: *mut Display,
-}
-
-impl Font {
- pub fn new(display: *mut Display, screen: i32, font_name: &str) -> Result<Self, X11Error> {
- let font_name_cstr =
- CString::new(font_name).map_err(|_| X11Error::FontLoadFailed(font_name.to_string()))?;
-
- let xft_font = unsafe { XftFontOpenName(display, screen, font_name_cstr.as_ptr()) };
-
- if xft_font.is_null() {
- return Err(X11Error::FontLoadFailed(font_name.to_string()));
- }
-
- Ok(Font { xft_font, display })
- }
-
- pub fn height(&self) -> u16 {
- unsafe {
- let font = &*self.xft_font;
- font.height as u16
- }
- }
-
- pub fn ascent(&self) -> i16 {
- unsafe {
- let font = &*self.xft_font;
- font.ascent as i16
- }
- }
-
- pub fn text_width(&self, text: &str) -> u16 {
- unsafe {
- let mut extents = std::mem::zeroed();
- x11::xft::XftTextExtentsUtf8(
- self.display,
- self.xft_font,
- text.as_ptr(),
- text.len() as i32,
- &mut extents,
- );
- extents.width
- }
- }
-}
-
-impl Drop for Font {
- fn drop(&mut self) {
- unsafe {
- if !self.xft_font.is_null() {
- x11::xft::XftFontClose(self.display, self.xft_font);
- }
- }
- }
-}
-
-pub struct FontDraw {
- xft_draw: *mut XftDraw,
-}
-
-impl FontDraw {
- pub fn new(
- display: *mut Display,
- drawable: Drawable,
- visual: *mut Visual,
- colormap: Colormap,
- ) -> Result<Self, X11Error> {
- let xft_draw = unsafe { x11::xft::XftDrawCreate(display, drawable, visual, colormap) };
-
- if xft_draw.is_null() {
- return Err(X11Error::DrawCreateFailed);
- }
-
- Ok(FontDraw { xft_draw })
- }
-
- pub fn draw_text(&self, font: &Font, color: u32, x: i16, y: i16, text: &str) {
- let red = ((color >> 16) & 0xFF) as u16;
- let green = ((color >> 8) & 0xFF) as u16;
- let blue = (color & 0xFF) as u16;
-
- let render_color = XRenderColor {
- red: red << 8 | red,
- green: green << 8 | green,
- blue: blue << 8 | blue,
- alpha: 0xFFFF,
- };
-
- let mut xft_color: XftColor = unsafe { std::mem::zeroed() };
-
- unsafe {
- x11::xft::XftColorAllocValue(
- x11::xft::XftDrawDisplay(self.xft_draw),
- x11::xft::XftDrawVisual(self.xft_draw),
- x11::xft::XftDrawColormap(self.xft_draw),
- &render_color,
- &mut xft_color,
- );
-
- XftDrawStringUtf8(
- self.xft_draw,
- &xft_color,
- font.xft_font,
- x as i32,
- y as i32,
- text.as_ptr(),
- text.len() as i32,
- );
-
- x11::xft::XftColorFree(
- x11::xft::XftDrawDisplay(self.xft_draw),
- x11::xft::XftDrawVisual(self.xft_draw),
- x11::xft::XftDrawColormap(self.xft_draw),
- &mut xft_color,
- );
- }
- }
-
- pub fn flush(&self) {
- unsafe {
- let display = x11::xft::XftDrawDisplay(self.xft_draw);
- x11::xlib::XFlush(display);
- }
- }
-
- pub fn sync(&self) {
- unsafe {
- let display = x11::xft::XftDrawDisplay(self.xft_draw);
- x11::xlib::XSync(display, 1);
- }
- }
-}
-
-impl Drop for FontDraw {
- fn drop(&mut self) {
- unsafe {
- if !self.xft_draw.is_null() {
- x11::xft::XftDrawDestroy(self.xft_draw);
- }
- }
- }
-}
-
-pub struct DrawingSurface {
- font_draw: FontDraw,
- pixmap: x11::xlib::Pixmap,
- display: *mut Display,
-}
-
-impl DrawingSurface {
- pub fn new(
- display: *mut Display,
- window: x11::xlib::Drawable,
- width: u32,
- height: u32,
- visual: *mut Visual,
- colormap: Colormap,
- ) -> Result<Self, crate::errors::X11Error> {
- let depth = unsafe { x11::xlib::XDefaultDepth(display, 0) };
- let pixmap = unsafe {
- x11::xlib::XCreatePixmap(display, window, width, height, depth as u32)
- };
-
- let font_draw = FontDraw::new(display, pixmap, visual, colormap)?;
-
- Ok(Self {
- font_draw,
- pixmap,
- display,
- })
- }
-
- pub fn pixmap(&self) -> x11::xlib::Pixmap {
- self.pixmap
- }
-
- pub fn font_draw(&self) -> &FontDraw {
- &self.font_draw
- }
-}
-
-impl Drop for DrawingSurface {
- fn drop(&mut self) {
- unsafe {
- x11::xft::XftDrawDestroy(self.font_draw.xft_draw);
- self.font_draw.xft_draw = std::ptr::null_mut();
- x11::xlib::XFreePixmap(self.display, self.pixmap);
- }
- }
-}
diff --git a/src/bar/mod.rs b/src/bar/mod.rs
deleted file mode 100644
index a60db74..0000000
--- a/src/bar/mod.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-mod bar;
-mod blocks;
-pub mod font;
-
-pub use bar::Bar;
-pub use blocks::{BlockCommand, BlockConfig};
-
-// Bar position (for future use)
-#[derive(Debug, Clone, Copy)]
-pub enum BarPosition {
- Top,
- Bottom,
-}
diff --git a/src/bin/main.rs b/src/bin/main.rs
deleted file mode 100644
index 42ef91d..0000000
--- a/src/bin/main.rs
+++ /dev/null
@@ -1,192 +0,0 @@
-use oxwm::errors::ConfigError;
-use oxwm::errors::MainError;
-use std::path::Path;
-use std::path::PathBuf;
-
-const CONFIG_FILE: &str = "config.lua";
-const TEMPLATE: &str = include_str!("../../templates/config.lua");
-
-enum Args {
- Exit,
- Arguments(Vec<String>),
- Error(MainError),
-}
-
-fn main() -> Result<(), MainError> {
- let arguments = match process_args() {
- Args::Exit => return Ok(()),
- Args::Arguments(v) => v,
- Args::Error(e) => return Err(e),
- };
-
- let (config, config_warning) = load_config(arguments.get(2))?;
-
- let mut window_manager = match oxwm::window_manager::WindowManager::new(config) {
- Ok(wm) => wm,
- Err(e) => return Err(MainError::CouldNotStartWm(e)),
- };
-
- if let Some(warning) = config_warning {
- window_manager.show_startup_config_error(warning);
- }
-
- if let Err(e) = window_manager.run() {
- return Err(MainError::WmError(e));
- }
-
- Ok(())
-}
-
-fn load_config(
- config_path: Option<&String>,
-) -> Result<(oxwm::Config, Option<ConfigError>), MainError> {
- let path = match config_path {
- None => {
- let config_dir = get_config_path()?;
- let config_path = config_dir.join(CONFIG_FILE);
- check_convert(&config_path)?;
- config_path
- }
- Some(p) => PathBuf::from(p),
- };
-
- let config_string = match std::fs::read_to_string(&path) {
- Ok(c) => c,
- Err(e) => return Err(MainError::FailedReadConfig(e)),
- };
-
- let config_directory = path.parent();
-
- let (mut config, config_warning) =
- match oxwm::config::parse_lua_config(&config_string, config_directory) {
- Ok(config) => (config, None),
- Err(warning) => {
- let config = match oxwm::config::parse_lua_config(TEMPLATE, None) {
- Ok(c) => c,
- Err(e) => return Err(MainError::FailedReadConfigTemplate(e)),
- };
- (config, Some(warning))
- }
- };
- config.path = Some(path);
- Ok((config, config_warning))
-}
-
-fn init_config() -> Result<(), MainError> {
- let config_directory = get_config_path()?;
- if let Err(e) = std::fs::create_dir_all(&config_directory) {
- return Err(MainError::CouldNotCreateConfigDir(e));
- }
-
- let config_template = TEMPLATE;
- let config_path = config_directory.join(CONFIG_FILE);
- if let Err(e) = std::fs::write(&config_path, config_template) {
- return Err(MainError::CouldNotWriteConfig(e));
- }
-
- println!("✓ Config created at {:?}", config_path);
- println!(" Edit the file and reload with Mod+Shift+R");
- println!(" No compilation needed - changes take effect immediately!");
-
- Ok(())
-}
-
-fn get_config_path() -> Result<PathBuf, MainError> {
- match dirs::config_dir() {
- Some(p) => Ok(p.join("oxwm")),
- None => Err(MainError::NoConfigDir),
- }
-}
-
-fn print_help() {
- println!("OXWM - A dynamic window manager written in Rust\n");
- println!("USAGE:");
- println!(" oxwm [OPTIONS]\n");
- println!("OPTIONS:");
- println!(" --init Create default config in ~/.config/oxwm/config.lua");
- println!(" --config <PATH> Use custom config file");
- println!(" --version Print version information");
- println!(" --help Print this help message\n");
- println!("CONFIG:");
- println!(" Location: ~/.config/oxwm/config.lua");
- println!(" Edit the config file and use Mod+Shift+R to reload");
- println!(" No compilation needed - instant hot-reload!");
- println!(" LSP support included with oxwm.lua type definitions\n");
- println!("FIRST RUN:");
- println!(" Run 'oxwm --init' to create a config file");
- println!(" Or just start oxwm and it will create one automatically\n");
-}
-
-fn process_args() -> Args {
- let mut args = std::env::args();
- let name = match args.next() {
- Some(n) => n,
- None => return Args::Error(MainError::NoProgramName),
- };
- let switch = args.next();
- let path = args.next();
-
- let switch = match switch {
- Some(s) => s,
- None => return Args::Arguments(vec![name]),
- };
-
- match switch.as_str() {
- "--version" => {
- println!("{name} {}", env!("CARGO_PKG_VERSION"));
- Args::Exit
- }
- "--help" => {
- print_help();
- Args::Exit
- }
- "--init" => match init_config() {
- Ok(_) => Args::Exit,
- Err(e) => Args::Error(e),
- },
- "--config" => match check_custom_config(path) {
- Ok(p) => Args::Arguments(vec![name, switch, p]),
- Err(e) => Args::Error(e),
- },
- _ => Args::Error(MainError::InvalidArguments),
- }
-}
-
-fn check_custom_config(path: Option<String>) -> Result<String, MainError> {
- let path = match path {
- Some(p) => p,
- None => {
- return Err(MainError::NoConfigPath);
- }
- };
-
- match std::fs::exists(&path) {
- Ok(b) => match b {
- true => Ok(path),
- false => Err(MainError::BadConfigPath),
- },
- Err(e) => Err(MainError::FailedCheckExist(e)),
- }
-}
-
-fn check_convert(path: &Path) -> Result<(), MainError> {
- let config_directory = get_config_path()?;
-
- if !path.exists() {
- let ron_path = config_directory.join("config.ron");
- let had_ron_config = ron_path.exists();
-
- println!("No config found at {:?}", config_directory);
- println!("Creating default Lua config...");
- init_config()?;
-
- if had_ron_config {
- println!("\n NOTICE: OXWM has migrated to Lua configuration.");
- println!(" Your old config.ron has been preserved, but is no longer used.");
- println!(" Your settings have been reset to defaults.");
- println!(" Please manually port your configuration to the new Lua format.");
- println!(" See the new config.lua template for examples.\n");
- }
- }
- Ok(())
-}
diff --git a/src/client.zig b/src/client.zig
new file mode 100644
index 0000000..c92c51d
--- /dev/null
+++ b/src/client.zig
@@ -0,0 +1,259 @@
+const std = @import("std");
+const xlib = @import("x11/xlib.zig");
+const Monitor = @import("monitor.zig").Monitor;
+
+pub const Client = struct {
+ name: [256]u8 = std.mem.zeroes([256]u8),
+ min_aspect: f32 = 0,
+ max_aspect: f32 = 0,
+ x: i32 = 0,
+ y: i32 = 0,
+ width: i32 = 0,
+ height: i32 = 0,
+ old_x: i32 = 0,
+ old_y: i32 = 0,
+ old_width: i32 = 0,
+ old_height: i32 = 0,
+ base_width: i32 = 0,
+ base_height: i32 = 0,
+ increment_width: i32 = 0,
+ increment_height: i32 = 0,
+ max_width: i32 = 0,
+ max_height: i32 = 0,
+ min_width: i32 = 0,
+ min_height: i32 = 0,
+ hints_valid: bool = false,
+ border_width: i32 = 0,
+ old_border_width: i32 = 0,
+ tags: u32 = 0,
+ is_fixed: bool = false,
+ is_floating: bool = false,
+ is_urgent: bool = false,
+ never_focus: bool = false,
+ old_state: bool = false,
+ is_fullscreen: bool = false,
+ next: ?*Client = null,
+ stack_next: ?*Client = null,
+ monitor: ?*Monitor = null,
+ window: xlib.Window = 0,
+};
+
+var allocator: std.mem.Allocator = undefined;
+
+pub fn init(alloc: std.mem.Allocator) void {
+ allocator = alloc;
+}
+
+pub fn create(window: xlib.Window) ?*Client {
+ const client = allocator.create(Client) catch return null;
+ client.* = Client{ .window = window };
+ return client;
+}
+
+pub fn destroy(client: *Client) void {
+ allocator.destroy(client);
+}
+
+pub fn attach(client: *Client) void {
+ if (client.monitor) |monitor| {
+ client.next = monitor.clients;
+ monitor.clients = client;
+ }
+}
+
+pub fn detach(client: *Client) void {
+ if (client.monitor) |monitor| {
+ var current_ptr: *?*Client = &monitor.clients;
+ while (current_ptr.*) |current| {
+ if (current == client) {
+ current_ptr.* = client.next;
+ return;
+ }
+ current_ptr = ¤t.next;
+ }
+ }
+}
+
+pub fn attach_stack(client: *Client) void {
+ if (client.monitor) |monitor| {
+ client.stack_next = monitor.stack;
+ monitor.stack = client;
+ }
+}
+
+pub fn detach_stack(client: *Client) void {
+ if (client.monitor) |monitor| {
+ var current_ptr: *?*Client = &monitor.stack;
+ while (current_ptr.*) |current| {
+ if (current == client) {
+ current_ptr.* = client.stack_next;
+ return;
+ }
+ current_ptr = ¤t.stack_next;
+ }
+ }
+}
+
+pub fn window_to_client(window: xlib.Window) ?*Client {
+ const monitor_mod = @import("monitor.zig");
+ var current_monitor = monitor_mod.monitors;
+ while (current_monitor) |monitor| {
+ var current_client = monitor.clients;
+ while (current_client) |client| {
+ if (client.window == window) {
+ return client;
+ }
+ current_client = client.next;
+ }
+ current_monitor = monitor.next;
+ }
+ return null;
+}
+
+pub fn next_tiled(client: ?*Client) ?*Client {
+ var current = client;
+ while (current) |iter| {
+ if (!iter.is_floating and is_visible(iter)) {
+ return iter;
+ }
+ current = iter.next;
+ }
+ return null;
+}
+
+pub fn is_visible(client: *Client) bool {
+ if (client.monitor) |monitor| {
+ return (client.tags & monitor.tagset[monitor.sel_tags]) != 0;
+ }
+ return false;
+}
+
+pub fn is_visible_on_tag(client: *Client, tags: u32) bool {
+ return (client.tags & tags) != 0;
+}
+
+pub fn next_tagged(client: *Client) ?*Client {
+ const monitor = client.monitor orelse return null;
+ var walked = monitor.clients;
+ while (walked) |iter| {
+ if (!iter.is_floating and is_visible_on_tag(iter, client.tags)) {
+ return iter;
+ }
+ walked = iter.next;
+ }
+ return null;
+}
+
+pub fn attach_aside(client: *Client) void {
+ const at = next_tagged(client);
+ if (at == null) {
+ attach(client);
+ return;
+ }
+ client.next = at.?.next;
+ at.?.next = client;
+}
+
+pub fn count_tiled(monitor: *Monitor) u32 {
+ var count: u32 = 0;
+ var current = next_tiled(monitor.clients);
+ while (current) |client| {
+ count += 1;
+ current = next_tiled(client.next);
+ }
+ return count;
+}
+
+pub fn tiled_window_at(exclude: *Client, monitor: *Monitor, point_x: i32, point_y: i32) ?*Client {
+ const tags = monitor.tagset[monitor.sel_tags];
+ var current = monitor.clients;
+
+ while (current) |client| {
+ if (client != exclude and !client.is_floating and (client.tags & tags) != 0) {
+ const client_x = client.x;
+ const client_y = client.y;
+ const client_w = client.width + client.border_width * 2;
+ const client_h = client.height + client.border_width * 2;
+
+ if (point_x >= client_x and point_x < client_x + client_w and
+ point_y >= client_y and point_y < client_y + client_h)
+ {
+ return client;
+ }
+ }
+ current = client.next;
+ }
+ return null;
+}
+
+pub fn insert_before(client: *Client, target: *Client) void {
+ const monitor = target.monitor orelse return;
+ if (client.monitor != monitor) return;
+
+ detach(client);
+
+ if (monitor.clients == target) {
+ client.next = target;
+ monitor.clients = client;
+ return;
+ }
+
+ var current = monitor.clients;
+ while (current) |iter| {
+ if (iter.next == target) {
+ client.next = target;
+ iter.next = client;
+ return;
+ }
+ current = iter.next;
+ }
+}
+
+pub fn swap_clients(client_a: *Client, client_b: *Client) void {
+ const monitor = client_a.monitor orelse return;
+ if (client_b.monitor != monitor) return;
+
+ var prev_a: ?*Client = null;
+ var prev_b: ?*Client = null;
+ var iter = monitor.clients;
+
+ while (iter) |client| {
+ if (client.next == client_a) prev_a = client;
+ if (client.next == client_b) prev_b = client;
+ iter = client.next;
+ }
+
+ const next_a = client_a.next;
+ const next_b = client_b.next;
+
+ if (next_a == client_b) {
+ client_a.next = next_b;
+ client_b.next = client_a;
+ if (prev_a) |prev| {
+ prev.next = client_b;
+ } else {
+ monitor.clients = client_b;
+ }
+ } else if (next_b == client_a) {
+ client_b.next = next_a;
+ client_a.next = client_b;
+ if (prev_b) |prev| {
+ prev.next = client_a;
+ } else {
+ monitor.clients = client_a;
+ }
+ } else {
+ client_a.next = next_b;
+ client_b.next = next_a;
+ if (prev_a) |prev| {
+ prev.next = client_b;
+ } else {
+ monitor.clients = client_b;
+ }
+ if (prev_b) |prev| {
+ prev.next = client_a;
+ } else {
+ monitor.clients = client_a;
+ }
+ }
+}
diff --git a/src/client/mod.rs b/src/client/mod.rs
deleted file mode 100644
index 38c0645..0000000
--- a/src/client/mod.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-use x11rb::protocol::xproto::Window;
-
-pub type TagMask = u32;
-
-#[derive(Debug, Clone)]
-pub struct Client {
- pub name: String,
- pub min_aspect: f32,
- pub max_aspect: f32,
- pub x_position: i16,
- pub y_position: i16,
- pub width: u16,
- pub height: u16,
- pub old_x_position: i16,
- pub old_y_position: i16,
- pub old_width: u16,
- pub old_height: u16,
- pub base_width: i32,
- pub base_height: i32,
- pub increment_width: i32,
- pub increment_height: i32,
- pub max_width: i32,
- pub max_height: i32,
- pub min_width: i32,
- pub min_height: i32,
- pub hints_valid: bool,
- pub border_width: u16,
- pub old_border_width: u16,
- pub tags: TagMask,
- pub is_fixed: bool,
- pub is_floating: bool,
- pub is_urgent: bool,
- pub never_focus: bool,
- pub old_state: bool,
- pub is_fullscreen: bool,
- pub next: Option<Window>,
- pub stack_next: Option<Window>,
- pub monitor_index: usize,
- pub window: Window,
-}
-
-impl Client {
- pub fn new(window: Window, monitor_index: usize, tags: TagMask) -> Self {
- Self {
- name: String::new(),
- min_aspect: 0.0,
- max_aspect: 0.0,
- x_position: 0,
- y_position: 0,
- width: 0,
- height: 0,
- old_x_position: 0,
- old_y_position: 0,
- old_width: 0,
- old_height: 0,
- base_width: 0,
- base_height: 0,
- increment_width: 0,
- increment_height: 0,
- max_width: 0,
- max_height: 0,
- min_width: 0,
- min_height: 0,
- hints_valid: false,
- border_width: 0,
- old_border_width: 0,
- tags,
- is_fixed: false,
- is_floating: false,
- is_urgent: false,
- never_focus: false,
- old_state: false,
- is_fullscreen: false,
- next: None,
- stack_next: None,
- monitor_index,
- window,
- }
- }
-
- pub fn width_with_border(&self) -> u16 {
- self.width.saturating_add(2 * self.border_width)
- }
-
- pub fn height_with_border(&self) -> u16 {
- self.height.saturating_add(2 * self.border_width)
- }
-}
diff --git a/src/config/config.zig b/src/config/config.zig
new file mode 100644
index 0000000..a8986b8
--- /dev/null
+++ b/src/config/config.zig
@@ -0,0 +1,185 @@
+const std = @import("std");
+
+pub const Action = enum {
+ spawn_terminal,
+ spawn,
+ kill_client,
+ quit,
+ reload_config,
+ restart,
+ focus_next,
+ focus_prev,
+ move_next,
+ move_prev,
+ resize_master,
+ inc_master,
+ dec_master,
+ toggle_floating,
+ toggle_fullscreen,
+ toggle_gaps,
+ cycle_layout,
+ set_layout,
+ set_layout_tiling,
+ set_layout_floating,
+ view_tag,
+ view_next_tag,
+ view_prev_tag,
+ view_next_nonempty_tag,
+ view_prev_nonempty_tag,
+ move_to_tag,
+ toggle_view_tag,
+ toggle_tag,
+ focus_monitor,
+ send_to_monitor,
+ scroll_left,
+ scroll_right,
+};
+
+pub const Keybind = struct {
+ mod_mask: u32,
+ keysym: u64,
+ action: Action,
+ int_arg: i32 = 0,
+ str_arg: ?[]const u8 = null,
+};
+
+pub const Rule = struct {
+ class: ?[]const u8,
+ instance: ?[]const u8,
+ title: ?[]const u8,
+ tags: u32,
+ is_floating: bool,
+ monitor: i32,
+};
+
+pub const Block_Type = enum {
+ static,
+ datetime,
+ ram,
+ shell,
+ battery,
+ cpu_temp,
+};
+
+pub const Click_Target = enum {
+ client_win,
+ root_win,
+ tag_bar,
+};
+
+pub const Mouse_Action = enum {
+ move_mouse,
+ resize_mouse,
+ toggle_floating,
+};
+
+pub const Mouse_Button = struct {
+ click: Click_Target,
+ mod_mask: u32,
+ button: u32,
+ action: Mouse_Action,
+};
+
+pub const Block = struct {
+ block_type: Block_Type,
+ format: []const u8,
+ command: ?[]const u8 = null,
+ interval: u32,
+ color: u32,
+ underline: bool = true,
+ datetime_format: ?[]const u8 = null,
+ format_charging: ?[]const u8 = null,
+ format_discharging: ?[]const u8 = null,
+ format_full: ?[]const u8 = null,
+ battery_name: ?[]const u8 = null,
+ thermal_zone: ?[]const u8 = null,
+};
+
+pub const Color_Scheme = struct {
+ fg: u32 = 0xbbbbbb,
+ bg: u32 = 0x1a1b26,
+ border: u32 = 0x444444,
+};
+
+pub const Config = struct {
+ allocator: std.mem.Allocator,
+
+ terminal: []const u8 = "st",
+ font: []const u8 = "monospace:size=10",
+ tags: [9][]const u8 = .{ "1", "2", "3", "4", "5", "6", "7", "8", "9" },
+
+ border_width: i32 = 2,
+ border_focused: u32 = 0x6dade3,
+ border_unfocused: u32 = 0x444444,
+
+ gaps_enabled: bool = true,
+ smartgaps_enabled: bool = false,
+ gap_inner_h: i32 = 5,
+ gap_inner_v: i32 = 5,
+ gap_outer_h: i32 = 5,
+ gap_outer_v: i32 = 5,
+
+ modkey: u32 = (1 << 6),
+ auto_tile: bool = false,
+ tag_back_and_forth: bool = false,
+ hide_vacant_tags: bool = false,
+
+ layout_tile_symbol: []const u8 = "[]=",
+ layout_monocle_symbol: []const u8 = "[M]",
+ layout_floating_symbol: []const u8 = "><>",
+
+ scheme_normal: Color_Scheme = .{ .fg = 0xbbbbbb, .bg = 0x1a1b26, .border = 0x444444 },
+ scheme_selected: Color_Scheme = .{ .fg = 0x0db9d7, .bg = 0x1a1b26, .border = 0xad8ee6 },
+ scheme_occupied: Color_Scheme = .{ .fg = 0x0db9d7, .bg = 0x1a1b26, .border = 0x0db9d7 },
+ scheme_urgent: Color_Scheme = .{ .fg = 0xf7768e, .bg = 0x1a1b26, .border = 0xf7768e },
+
+ keybinds: std.ArrayListUnmanaged(Keybind) = .{},
+ rules: std.ArrayListUnmanaged(Rule) = .{},
+ blocks: std.ArrayListUnmanaged(Block) = .{},
+ buttons: std.ArrayListUnmanaged(Mouse_Button) = .{},
+ autostart: std.ArrayListUnmanaged([]const u8) = .{},
+
+ pub fn init(allocator: std.mem.Allocator) Config {
+ return Config{
+ .allocator = allocator,
+ };
+ }
+
+ pub fn deinit(self: *Config) void {
+ self.keybinds.deinit(self.allocator);
+ self.rules.deinit(self.allocator);
+ self.blocks.deinit(self.allocator);
+ self.buttons.deinit(self.allocator);
+ self.autostart.deinit(self.allocator);
+ }
+
+ pub fn add_keybind(self: *Config, keybind: Keybind) !void {
+ try self.keybinds.append(self.allocator, keybind);
+ }
+
+ pub fn add_rule(self: *Config, rule: Rule) !void {
+ try self.rules.append(self.allocator, rule);
+ }
+
+ pub fn add_block(self: *Config, block: Block) !void {
+ try self.blocks.append(self.allocator, block);
+ }
+
+ pub fn add_button(self: *Config, button: Mouse_Button) !void {
+ try self.buttons.append(self.allocator, button);
+ }
+
+ pub fn add_autostart(self: *Config, cmd: []const u8) !void {
+ try self.autostart.append(self.allocator, cmd);
+ }
+};
+
+pub var global_config: ?*Config = null;
+
+pub fn get_config() ?*Config {
+ return global_config;
+}
+
+pub fn set_config(cfg: *Config) void {
+ global_config = cfg;
+}
diff --git a/src/config/lua.rs b/src/config/lua.rs
deleted file mode 100644
index 69d4286..0000000
--- a/src/config/lua.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use crate::errors::ConfigError;
-use mlua::Lua;
-
-use super::lua_api;
-
-pub fn parse_lua_config(
- input: &str,
- config_dir: Option<&std::path::Path>,
-) -> Result<crate::Config, ConfigError> {
- let lua = Lua::new();
-
- if let Some(dir) = config_dir
- && let Some(dir_str) = dir.to_str()
- {
- let setup_code = format!("package.path = '{}/?.lua;' .. package.path", dir_str);
- lua.load(&setup_code)
- .exec()
- .map_err(|e| ConfigError::LuaError(format!("Failed to set package.path: {}", e)))?;
- }
-
- let builder = lua_api::register_api(&lua)?;
-
- lua.load(input)
- .exec()
- .map_err(|e| ConfigError::LuaError(format!("{}", e)))?;
-
- let builder_data = builder.borrow().clone();
-
- Ok(crate::Config {
- border_width: builder_data.border_width,
- border_focused: builder_data.border_focused,
- border_unfocused: builder_data.border_unfocused,
- font: builder_data.font,
- gaps_enabled: builder_data.gaps_enabled,
- smartgaps_enabled: builder_data.smartgaps_enabled,
- gap_inner_horizontal: builder_data.gap_inner_horizontal,
- gap_inner_vertical: builder_data.gap_inner_vertical,
- gap_outer_horizontal: builder_data.gap_outer_horizontal,
- gap_outer_vertical: builder_data.gap_outer_vertical,
- terminal: builder_data.terminal,
- modkey: builder_data.modkey,
- tags: builder_data.tags,
- layout_symbols: builder_data.layout_symbols,
- keybindings: builder_data.keybindings,
- tag_back_and_forth: builder_data.tag_back_and_forth,
- window_rules: builder_data.window_rules,
- status_blocks: builder_data.status_blocks,
- scheme_normal: builder_data.scheme_normal,
- scheme_occupied: builder_data.scheme_occupied,
- scheme_selected: builder_data.scheme_selected,
- scheme_urgent: builder_data.scheme_urgent,
- autostart: builder_data.autostart,
- auto_tile: builder_data.auto_tile,
- hide_vacant_tags: builder_data.hide_vacant_tags,
- path: None,
- })
-}
diff --git a/src/config/lua.zig b/src/config/lua.zig
new file mode 100644
index 0000000..96473f5
--- /dev/null
+++ b/src/config/lua.zig
@@ -0,0 +1,1172 @@
+const std = @import("std");
+const config_mod = @import("config.zig");
+const Config = config_mod.Config;
+const Keybind = config_mod.Keybind;
+const Action = config_mod.Action;
+const Rule = config_mod.Rule;
+const Block = config_mod.Block;
+const Block_Type = config_mod.Block_Type;
+const Mouse_Button = config_mod.Mouse_Button;
+const Click_Target = config_mod.Click_Target;
+const Mouse_Action = config_mod.Mouse_Action;
+const Color_Scheme = config_mod.Color_Scheme;
+
+const c = @cImport({
+ @cInclude("lua.h");
+ @cInclude("lauxlib.h");
+ @cInclude("lualib.h");
+});
+
+var L: ?*c.lua_State = null;
+var config: ?*Config = null;
+
+pub fn init(cfg: *Config) bool {
+ config = cfg;
+ L = c.luaL_newstate();
+ if (L == null) return false;
+ c.luaL_openlibs(L);
+ register_api();
+ return true;
+}
+
+pub fn deinit() void {
+ if (L) |state| {
+ c.lua_close(state);
+ }
+ L = null;
+ config = null;
+}
+
+pub fn load_file(path: []const u8) bool {
+ const state = L orelse return false;
+ var path_buf: [512]u8 = undefined;
+ if (path.len >= path_buf.len) return false;
+ @memcpy(path_buf[0..path.len], path);
+ path_buf[path.len] = 0;
+
+ if (c.luaL_loadfilex(state, &path_buf, null) != 0) {
+ const err = c.lua_tolstring(state, -1, null);
+ if (err != null) {
+ std.debug.print("lua load error: {s}\n", .{std.mem.span(err)});
+ }
+ c.lua_settop(state, -2);
+ return false;
+ }
+
+ if (c.lua_pcallk(state, 0, 0, 0, 0, null) != 0) {
+ const err = c.lua_tolstring(state, -1, null);
+ if (err != null) {
+ std.debug.print("lua runtime error: {s}\n", .{std.mem.span(err)});
+ }
+ c.lua_settop(state, -2);
+ return false;
+ }
+
+ return true;
+}
+
+pub fn load_config() bool {
+ const home = std.posix.getenv("HOME") orelse return false;
+ var path_buf: [512]u8 = undefined;
+ const path = std.fmt.bufPrint(&path_buf, "{s}/.config/oxwm/config.lua", .{home}) catch return false;
+ return load_file(path);
+}
+
+fn register_api() void {
+ const state = L orelse return;
+
+ c.lua_createtable(state, 0, 16);
+
+ register_spawn_functions(state);
+ register_key_module(state);
+ register_gaps_module(state);
+ register_border_module(state);
+ register_client_module(state);
+ register_layout_module(state);
+ register_tag_module(state);
+ register_monitor_module(state);
+ register_rule_module(state);
+ register_bar_module(state);
+ register_misc_functions(state);
+
+ c.lua_setglobal(state, "oxwm");
+}
+
+fn register_spawn_functions(state: *c.lua_State) void {
+ c.lua_pushcfunction(state, lua_spawn);
+ c.lua_setfield(state, -2, "spawn");
+
+ c.lua_pushcfunction(state, lua_spawn_terminal);
+ c.lua_setfield(state, -2, "spawn_terminal");
+}
+
+fn register_key_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 2);
+
+ c.lua_pushcfunction(state, lua_key_bind);
+ c.lua_setfield(state, -2, "bind");
+
+ c.lua_pushcfunction(state, lua_key_chord);
+ c.lua_setfield(state, -2, "chord");
+
+ c.lua_setfield(state, -2, "key");
+}
+
+fn register_gaps_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 6);
+
+ c.lua_pushcfunction(state, lua_gaps_set_enabled);
+ c.lua_setfield(state, -2, "set_enabled");
+
+ c.lua_pushcfunction(state, lua_gaps_enable);
+ c.lua_setfield(state, -2, "enable");
+
+ c.lua_pushcfunction(state, lua_gaps_disable);
+ c.lua_setfield(state, -2, "disable");
+
+ c.lua_pushcfunction(state, lua_gaps_set_inner);
+ c.lua_setfield(state, -2, "set_inner");
+
+ c.lua_pushcfunction(state, lua_gaps_set_outer);
+ c.lua_setfield(state, -2, "set_outer");
+
+ c.lua_pushcfunction(state, lua_gaps_set_smart);
+ c.lua_setfield(state, -2, "set_smart");
+
+ c.lua_setfield(state, -2, "gaps");
+}
+
+fn register_border_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 3);
+
+ c.lua_pushcfunction(state, lua_border_set_width);
+ c.lua_setfield(state, -2, "set_width");
+
+ c.lua_pushcfunction(state, lua_border_set_focused_color);
+ c.lua_setfield(state, -2, "set_focused_color");
+
+ c.lua_pushcfunction(state, lua_border_set_unfocused_color);
+ c.lua_setfield(state, -2, "set_unfocused_color");
+
+ c.lua_setfield(state, -2, "border");
+}
+
+fn register_client_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 5);
+
+ c.lua_pushcfunction(state, lua_client_kill);
+ c.lua_setfield(state, -2, "kill");
+
+ c.lua_pushcfunction(state, lua_client_toggle_fullscreen);
+ c.lua_setfield(state, -2, "toggle_fullscreen");
+
+ c.lua_pushcfunction(state, lua_client_toggle_floating);
+ c.lua_setfield(state, -2, "toggle_floating");
+
+ c.lua_pushcfunction(state, lua_client_focus_stack);
+ c.lua_setfield(state, -2, "focus_stack");
+
+ c.lua_pushcfunction(state, lua_client_move_stack);
+ c.lua_setfield(state, -2, "move_stack");
+
+ c.lua_setfield(state, -2, "client");
+}
+
+fn register_layout_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 4);
+
+ c.lua_pushcfunction(state, lua_layout_cycle);
+ c.lua_setfield(state, -2, "cycle");
+
+ c.lua_pushcfunction(state, lua_layout_set);
+ c.lua_setfield(state, -2, "set");
+
+ c.lua_pushcfunction(state, lua_layout_scroll_left);
+ c.lua_setfield(state, -2, "scroll_left");
+
+ c.lua_pushcfunction(state, lua_layout_scroll_right);
+ c.lua_setfield(state, -2, "scroll_right");
+
+ c.lua_setfield(state, -2, "layout");
+}
+
+fn register_tag_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 10);
+
+ c.lua_pushcfunction(state, lua_tag_view);
+ c.lua_setfield(state, -2, "view");
+
+ c.lua_pushcfunction(state, lua_tag_view_next);
+ c.lua_setfield(state, -2, "view_next");
+
+ c.lua_pushcfunction(state, lua_tag_view_previous);
+ c.lua_setfield(state, -2, "view_previous");
+
+ c.lua_pushcfunction(state, lua_tag_view_next_nonempty);
+ c.lua_setfield(state, -2, "view_next_nonempty");
+
+ c.lua_pushcfunction(state, lua_tag_view_previous_nonempty);
+ c.lua_setfield(state, -2, "view_previous_nonempty");
+
+ c.lua_pushcfunction(state, lua_tag_toggleview);
+ c.lua_setfield(state, -2, "toggleview");
+
+ c.lua_pushcfunction(state, lua_tag_move_to);
+ c.lua_setfield(state, -2, "move_to");
+
+ c.lua_pushcfunction(state, lua_tag_toggletag);
+ c.lua_setfield(state, -2, "toggletag");
+
+ c.lua_pushcfunction(state, lua_tag_set_back_and_forth);
+ c.lua_setfield(state, -2, "set_back_and_forth");
+
+ c.lua_setfield(state, -2, "tag");
+}
+
+fn register_monitor_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 2);
+
+ c.lua_pushcfunction(state, lua_monitor_focus);
+ c.lua_setfield(state, -2, "focus");
+
+ c.lua_pushcfunction(state, lua_monitor_tag);
+ c.lua_setfield(state, -2, "tag");
+
+ c.lua_setfield(state, -2, "monitor");
+}
+
+fn register_rule_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 1);
+
+ c.lua_pushcfunction(state, lua_rule_add);
+ c.lua_setfield(state, -2, "add");
+
+ c.lua_setfield(state, -2, "rule");
+}
+
+fn register_bar_module(state: *c.lua_State) void {
+ c.lua_createtable(state, 0, 10);
+
+ c.lua_pushcfunction(state, lua_bar_set_font);
+ c.lua_setfield(state, -2, "set_font");
+
+ c.lua_pushcfunction(state, lua_bar_set_blocks);
+ c.lua_setfield(state, -2, "set_blocks");
+
+ c.lua_pushcfunction(state, lua_bar_set_scheme_normal);
+ c.lua_setfield(state, -2, "set_scheme_normal");
+
+ c.lua_pushcfunction(state, lua_bar_set_scheme_selected);
+ c.lua_setfield(state, -2, "set_scheme_selected");
+
+ c.lua_pushcfunction(state, lua_bar_set_scheme_occupied);
+ c.lua_setfield(state, -2, "set_scheme_occupied");
+
+ c.lua_pushcfunction(state, lua_bar_set_scheme_urgent);
+ c.lua_setfield(state, -2, "set_scheme_urgent");
+
+ c.lua_pushcfunction(state, lua_bar_set_hide_vacant_tags);
+ c.lua_setfield(state, -2, "set_hide_vacant_tags");
+
+ c.lua_createtable(state, 0, 6);
+
+ c.lua_pushcfunction(state, lua_bar_block_ram);
+ c.lua_setfield(state, -2, "ram");
+
+ c.lua_pushcfunction(state, lua_bar_block_datetime);
+ c.lua_setfield(state, -2, "datetime");
+
+ c.lua_pushcfunction(state, lua_bar_block_shell);
+ c.lua_setfield(state, -2, "shell");
+
+ c.lua_pushcfunction(state, lua_bar_block_static);
+ c.lua_setfield(state, -2, "static");
+
+ c.lua_pushcfunction(state, lua_bar_block_battery);
+ c.lua_setfield(state, -2, "battery");
+
+ c.lua_setfield(state, -2, "block");
+
+ c.lua_setfield(state, -2, "bar");
+}
+
+fn register_misc_functions(state: *c.lua_State) void {
+ c.lua_pushcfunction(state, lua_set_terminal);
+ c.lua_setfield(state, -2, "set_terminal");
+
+ c.lua_pushcfunction(state, lua_set_modkey);
+ c.lua_setfield(state, -2, "set_modkey");
+
+ c.lua_pushcfunction(state, lua_set_tags);
+ c.lua_setfield(state, -2, "set_tags");
+
+ c.lua_pushcfunction(state, lua_autostart);
+ c.lua_setfield(state, -2, "autostart");
+
+ c.lua_pushcfunction(state, lua_auto_tile);
+ c.lua_setfield(state, -2, "auto_tile");
+
+ c.lua_pushcfunction(state, lua_quit);
+ c.lua_setfield(state, -2, "quit");
+
+ c.lua_pushcfunction(state, lua_restart);
+ c.lua_setfield(state, -2, "restart");
+
+ c.lua_pushcfunction(state, lua_toggle_gaps);
+ c.lua_setfield(state, -2, "toggle_gaps");
+
+ c.lua_pushcfunction(state, lua_set_master_factor);
+ c.lua_setfield(state, -2, "set_master_factor");
+
+ c.lua_pushcfunction(state, lua_inc_num_master);
+ c.lua_setfield(state, -2, "inc_num_master");
+}
+
+fn create_action_table(state: *c.lua_State, action_name: [*:0]const u8) void {
+ c.lua_createtable(state, 0, 2);
+ _ = c.lua_pushstring(state, action_name);
+ c.lua_setfield(state, -2, "__action");
+}
+
+fn create_action_table_with_int(state: *c.lua_State, action_name: [*:0]const u8, arg: i32) void {
+ c.lua_createtable(state, 0, 2);
+ _ = c.lua_pushstring(state, action_name);
+ c.lua_setfield(state, -2, "__action");
+ c.lua_pushinteger(state, arg);
+ c.lua_setfield(state, -2, "__arg");
+}
+
+fn create_action_table_with_string(state: *c.lua_State, action_name: [*:0]const u8) void {
+ c.lua_createtable(state, 0, 2);
+ _ = c.lua_pushstring(state, action_name);
+ c.lua_setfield(state, -2, "__action");
+ c.lua_pushvalue(state, 1);
+ c.lua_setfield(state, -2, "__arg");
+}
+
+fn lua_spawn(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table_with_string(s, "Spawn");
+ return 1;
+}
+
+fn lua_spawn_terminal(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "SpawnTerminal");
+ return 1;
+}
+
+fn lua_key_bind(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const cfg = config orelse return 0;
+
+ const mod_mask = parse_modifiers(s, 1);
+ const key_str = get_string_arg(s, 2) orelse return 0;
+ const keysym = key_name_to_keysym(key_str) orelse return 0;
+
+ if (c.lua_type(s, 3) != c.LUA_TTABLE) return 0;
+
+ _ = c.lua_getfield(s, 3, "__action");
+ const action_str = get_lua_string(s, -1) orelse {
+ c.lua_settop(s, -2);
+ return 0;
+ };
+ c.lua_settop(s, -2);
+
+ const action = parse_action(action_str) orelse return 0;
+
+ var int_arg: i32 = 0;
+ var str_arg: ?[]const u8 = null;
+
+ _ = c.lua_getfield(s, 3, "__arg");
+ if (c.lua_type(s, -1) == c.LUA_TNUMBER) {
+ int_arg = @intCast(c.lua_tointegerx(s, -1, null));
+ } else if (c.lua_type(s, -1) == c.LUA_TSTRING) {
+ str_arg = get_lua_string(s, -1);
+ }
+ c.lua_settop(s, -2);
+
+ cfg.add_keybind(.{
+ .mod_mask = mod_mask,
+ .keysym = keysym,
+ .action = action,
+ .int_arg = int_arg,
+ .str_arg = str_arg,
+ }) catch return 0;
+
+ return 0;
+}
+
+fn lua_key_chord(state: ?*c.lua_State) callconv(.c) c_int {
+ _ = state;
+ return 0;
+}
+
+fn lua_gaps_set_enabled(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.gaps_enabled = c.lua_toboolean(s, 1) != 0;
+ return 0;
+}
+
+fn lua_gaps_enable(state: ?*c.lua_State) callconv(.c) c_int {
+ _ = state;
+ const cfg = config orelse return 0;
+ cfg.gaps_enabled = true;
+ return 0;
+}
+
+fn lua_gaps_disable(state: ?*c.lua_State) callconv(.c) c_int {
+ _ = state;
+ const cfg = config orelse return 0;
+ cfg.gaps_enabled = false;
+ return 0;
+}
+
+fn lua_gaps_set_inner(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.gap_inner_h = @intCast(c.lua_tointegerx(s, 1, null));
+ cfg.gap_inner_v = @intCast(c.lua_tointegerx(s, 2, null));
+ return 0;
+}
+
+fn lua_gaps_set_outer(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.gap_outer_h = @intCast(c.lua_tointegerx(s, 1, null));
+ cfg.gap_outer_v = @intCast(c.lua_tointegerx(s, 2, null));
+ return 0;
+}
+
+fn lua_gaps_set_smart(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.smartgaps_enabled = c.lua_toboolean(s, 1) != 0;
+ return 0;
+}
+
+fn lua_border_set_width(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.border_width = @intCast(c.lua_tointegerx(s, 1, null));
+ return 0;
+}
+
+fn lua_border_set_focused_color(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.border_focused = parse_color(s, 1);
+ return 0;
+}
+
+fn lua_border_set_unfocused_color(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.border_unfocused = parse_color(s, 1);
+ return 0;
+}
+
+fn lua_client_kill(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "KillClient");
+ return 1;
+}
+
+fn lua_client_toggle_fullscreen(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ToggleFullScreen");
+ return 1;
+}
+
+fn lua_client_toggle_floating(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ToggleFloating");
+ return 1;
+}
+
+fn lua_client_focus_stack(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const dir: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "FocusStack", dir);
+ return 1;
+}
+
+fn lua_client_move_stack(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const dir: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "MoveStack", dir);
+ return 1;
+}
+
+fn lua_layout_cycle(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "CycleLayout");
+ return 1;
+}
+
+fn lua_layout_set(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table_with_string(s, "ChangeLayout");
+ return 1;
+}
+
+fn lua_layout_scroll_left(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ScrollLeft");
+ return 1;
+}
+
+fn lua_layout_scroll_right(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ScrollRight");
+ return 1;
+}
+
+fn lua_tag_view(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "ViewTag", idx - 1);
+ return 1;
+}
+
+fn lua_tag_view_next(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ViewNextTag");
+ return 1;
+}
+
+fn lua_tag_view_previous(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ViewPreviousTag");
+ return 1;
+}
+
+fn lua_tag_view_next_nonempty(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ViewNextNonEmptyTag");
+ return 1;
+}
+
+fn lua_tag_view_previous_nonempty(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ViewPreviousNonEmptyTag");
+ return 1;
+}
+
+fn lua_tag_toggleview(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "ToggleView", idx - 1);
+ return 1;
+}
+
+fn lua_tag_move_to(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "MoveToTag", idx - 1);
+ return 1;
+}
+
+fn lua_tag_toggletag(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const idx: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "ToggleTag", idx - 1);
+ return 1;
+}
+
+fn lua_tag_set_back_and_forth(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.tag_back_and_forth = c.lua_toboolean(s, 1) != 0;
+ return 0;
+}
+
+fn lua_monitor_focus(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const dir: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "FocusMonitor", dir);
+ return 1;
+}
+
+fn lua_monitor_tag(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const dir: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "TagMonitor", dir);
+ return 1;
+}
+
+fn lua_rule_add(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+
+ if (c.lua_type(s, 1) != c.LUA_TTABLE) return 0;
+
+ var rule = Rule{
+ .class = null,
+ .instance = null,
+ .title = null,
+ .tags = 0,
+ .is_floating = false,
+ .monitor = -1,
+ };
+
+ _ = c.lua_getfield(s, 1, "class");
+ if (c.lua_type(s, -1) == c.LUA_TSTRING) {
+ rule.class = get_lua_string(s, -1);
+ }
+ c.lua_settop(s, -2);
+
+ _ = c.lua_getfield(s, 1, "instance");
+ if (c.lua_type(s, -1) == c.LUA_TSTRING) {
+ rule.instance = get_lua_string(s, -1);
+ }
+ c.lua_settop(s, -2);
+
+ _ = c.lua_getfield(s, 1, "title");
+ if (c.lua_type(s, -1) == c.LUA_TSTRING) {
+ rule.title = get_lua_string(s, -1);
+ }
+ c.lua_settop(s, -2);
+
+ _ = c.lua_getfield(s, 1, "tag");
+ if (c.lua_type(s, -1) == c.LUA_TNUMBER) {
+ const tag_idx: i32 = @intCast(c.lua_tointegerx(s, -1, null));
+ if (tag_idx > 0) {
+ rule.tags = @as(u32, 1) << @intCast(tag_idx - 1);
+ }
+ }
+ c.lua_settop(s, -2);
+
+ _ = c.lua_getfield(s, 1, "floating");
+ if (c.lua_type(s, -1) == c.LUA_TBOOLEAN) {
+ rule.is_floating = c.lua_toboolean(s, -1) != 0;
+ }
+ c.lua_settop(s, -2);
+
+ _ = c.lua_getfield(s, 1, "monitor");
+ if (c.lua_type(s, -1) == c.LUA_TNUMBER) {
+ rule.monitor = @intCast(c.lua_tointegerx(s, -1, null));
+ }
+ c.lua_settop(s, -2);
+
+ cfg.add_rule(rule) catch return 0;
+ return 0;
+}
+
+fn lua_bar_set_font(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ if (get_string_arg(s, 1)) |font| {
+ cfg.font = font;
+ }
+ return 0;
+}
+
+fn lua_bar_set_blocks(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+
+ if (c.lua_type(s, 1) != c.LUA_TTABLE) return 0;
+
+ const len = c.lua_rawlen(s, 1);
+ var i: usize = 1;
+ while (i <= len) : (i += 1) {
+ _ = c.lua_rawgeti(s, 1, @intCast(i));
+
+ if (c.lua_type(s, -1) != c.LUA_TTABLE) {
+ c.lua_settop(s, -2);
+ continue;
+ }
+
+ if (parse_block_config(s, -1)) |block| {
+ cfg.add_block(block) catch {};
+ }
+
+ c.lua_settop(s, -2);
+ }
+
+ return 0;
+}
+
+fn parse_block_config(state: *c.lua_State, idx: c_int) ?Block {
+ _ = c.lua_getfield(state, idx, "__block_type");
+ const block_type_str = get_lua_string(state, -1) orelse {
+ c.lua_settop(state, -2);
+ return null;
+ };
+ c.lua_settop(state, -2);
+
+ _ = c.lua_getfield(state, idx, "format");
+ const format = get_lua_string(state, -1) orelse "";
+ c.lua_settop(state, -2);
+
+ _ = c.lua_getfield(state, idx, "interval");
+ const interval: u32 = @intCast(c.lua_tointegerx(state, -1, null));
+ c.lua_settop(state, -2);
+
+ _ = c.lua_getfield(state, idx, "color");
+ const color = parse_color(state, -1);
+ c.lua_settop(state, -2);
+
+ _ = c.lua_getfield(state, idx, "underline");
+ const underline = c.lua_toboolean(state, -1) != 0;
+ c.lua_settop(state, -2);
+
+ var block = Block{
+ .block_type = .static,
+ .format = format,
+ .interval = interval,
+ .color = color,
+ .underline = underline,
+ };
+
+ if (std.mem.eql(u8, block_type_str, "Ram")) {
+ block.block_type = .ram;
+ } else if (std.mem.eql(u8, block_type_str, "DateTime")) {
+ block.block_type = .datetime;
+ _ = c.lua_getfield(state, idx, "__arg");
+ block.datetime_format = get_lua_string(state, -1);
+ c.lua_settop(state, -2);
+ } else if (std.mem.eql(u8, block_type_str, "Shell")) {
+ block.block_type = .shell;
+ _ = c.lua_getfield(state, idx, "__arg");
+ block.command = get_lua_string(state, -1);
+ c.lua_settop(state, -2);
+ } else if (std.mem.eql(u8, block_type_str, "Static")) {
+ block.block_type = .static;
+ _ = c.lua_getfield(state, idx, "__arg");
+ if (get_lua_string(state, -1)) |text| {
+ block.format = text;
+ }
+ c.lua_settop(state, -2);
+ } else if (std.mem.eql(u8, block_type_str, "Battery")) {
+ block.block_type = .battery;
+ _ = c.lua_getfield(state, idx, "__arg");
+ if (c.lua_type(state, -1) == c.LUA_TTABLE) {
+ _ = c.lua_getfield(state, -1, "charging");
+ block.format_charging = get_lua_string(state, -1);
+ c.lua_settop(state, -2);
+
+ _ = c.lua_getfield(state, -1, "discharging");
+ block.format_discharging = get_lua_string(state, -1);
+ c.lua_settop(state, -2);
+
+ _ = c.lua_getfield(state, -1, "full");
+ block.format_full = get_lua_string(state, -1);
+ c.lua_settop(state, -2);
+
+ _ = c.lua_getfield(state, -1, "battery_name");
+ block.battery_name = get_lua_string(state, -1);
+ c.lua_settop(state, -2);
+ }
+ c.lua_settop(state, -2);
+ } else {
+ return null;
+ }
+
+ return block;
+}
+
+fn lua_bar_set_scheme_normal(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.scheme_normal = parse_scheme(s);
+ return 0;
+}
+
+fn lua_bar_set_scheme_selected(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.scheme_selected = parse_scheme(s);
+ return 0;
+}
+
+fn lua_bar_set_scheme_occupied(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.scheme_occupied = parse_scheme(s);
+ return 0;
+}
+
+fn lua_bar_set_scheme_urgent(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.scheme_urgent = parse_scheme(s);
+ return 0;
+}
+
+fn lua_bar_set_hide_vacant_tags(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.hide_vacant_tags = c.lua_toboolean(s, 1) != 0;
+ return 0;
+}
+
+fn lua_bar_block_ram(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_block_table(s, "Ram", null);
+ return 1;
+}
+
+fn lua_bar_block_datetime(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ _ = c.lua_getfield(s, 1, "date_format");
+ const date_format = get_lua_string(s, -1);
+ c.lua_settop(s, -2);
+ create_block_table(s, "DateTime", date_format);
+ return 1;
+}
+
+fn lua_bar_block_shell(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ _ = c.lua_getfield(s, 1, "command");
+ const command = get_lua_string(s, -1);
+ c.lua_settop(s, -2);
+ create_block_table(s, "Shell", command);
+ return 1;
+}
+
+fn lua_bar_block_static(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ _ = c.lua_getfield(s, 1, "text");
+ const text = get_lua_string(s, -1);
+ c.lua_settop(s, -2);
+ create_block_table(s, "Static", text);
+ return 1;
+}
+
+fn lua_bar_block_battery(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+
+ c.lua_createtable(s, 0, 6);
+
+ _ = c.lua_pushstring(s, "Battery");
+ c.lua_setfield(s, -2, "__block_type");
+
+ _ = c.lua_getfield(s, 1, "format");
+ c.lua_setfield(s, -2, "format");
+
+ _ = c.lua_getfield(s, 1, "interval");
+ c.lua_setfield(s, -2, "interval");
+
+ _ = c.lua_getfield(s, 1, "color");
+ c.lua_setfield(s, -2, "color");
+
+ _ = c.lua_getfield(s, 1, "underline");
+ c.lua_setfield(s, -2, "underline");
+
+ c.lua_createtable(s, 0, 4);
+ _ = c.lua_getfield(s, 1, "charging");
+ c.lua_setfield(s, -2, "charging");
+ _ = c.lua_getfield(s, 1, "discharging");
+ c.lua_setfield(s, -2, "discharging");
+ _ = c.lua_getfield(s, 1, "full");
+ c.lua_setfield(s, -2, "full");
+ _ = c.lua_getfield(s, 1, "battery_name");
+ c.lua_setfield(s, -2, "battery_name");
+ c.lua_setfield(s, -2, "__arg");
+
+ return 1;
+}
+
+fn create_block_table(state: *c.lua_State, block_type: [*:0]const u8, arg: ?[]const u8) void {
+ c.lua_createtable(state, 0, 6);
+
+ _ = c.lua_pushstring(state, block_type);
+ c.lua_setfield(state, -2, "__block_type");
+
+ _ = c.lua_getfield(state, 1, "format");
+ c.lua_setfield(state, -2, "format");
+
+ _ = c.lua_getfield(state, 1, "interval");
+ c.lua_setfield(state, -2, "interval");
+
+ _ = c.lua_getfield(state, 1, "color");
+ c.lua_setfield(state, -2, "color");
+
+ _ = c.lua_getfield(state, 1, "underline");
+ c.lua_setfield(state, -2, "underline");
+
+ if (arg) |a| {
+ var buf: [256]u8 = undefined;
+ if (a.len < buf.len) {
+ @memcpy(buf[0..a.len], a);
+ buf[a.len] = 0;
+ _ = c.lua_pushstring(state, &buf);
+ c.lua_setfield(state, -2, "__arg");
+ }
+ }
+}
+
+fn lua_set_terminal(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ if (get_string_arg(s, 1)) |term| {
+ cfg.terminal = term;
+ }
+ return 0;
+}
+
+fn lua_set_modkey(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ if (get_string_arg(s, 1)) |modkey_str| {
+ cfg.modkey = parse_single_modifier(modkey_str);
+ }
+ return 0;
+}
+
+fn lua_set_tags(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+
+ if (c.lua_type(s, 1) != c.LUA_TTABLE) return 0;
+
+ const len = c.lua_rawlen(s, 1);
+ var i: usize = 0;
+ while (i < len and i < 9) : (i += 1) {
+ _ = c.lua_rawgeti(s, 1, @intCast(i + 1));
+ if (get_lua_string(s, -1)) |tag_str| {
+ cfg.tags[i] = tag_str;
+ }
+ c.lua_settop(s, -2);
+ }
+
+ return 0;
+}
+
+fn lua_autostart(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ if (get_string_arg(s, 1)) |cmd| {
+ cfg.add_autostart(cmd) catch return 0;
+ }
+ return 0;
+}
+
+fn lua_auto_tile(state: ?*c.lua_State) callconv(.c) c_int {
+ const cfg = config orelse return 0;
+ const s = state orelse return 0;
+ cfg.auto_tile = c.lua_toboolean(s, 1) != 0;
+ return 0;
+}
+
+fn lua_quit(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "Quit");
+ return 1;
+}
+
+fn lua_restart(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "Restart");
+ return 1;
+}
+
+fn lua_toggle_gaps(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ create_action_table(s, "ToggleGaps");
+ return 1;
+}
+
+fn lua_set_master_factor(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const delta: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ create_action_table_with_int(s, "ResizeMaster", delta);
+ return 1;
+}
+
+fn lua_inc_num_master(state: ?*c.lua_State) callconv(.c) c_int {
+ const s = state orelse return 0;
+ const delta: i32 = @intCast(c.lua_tointegerx(s, 1, null));
+ if (delta > 0) {
+ create_action_table(s, "IncMaster");
+ } else {
+ create_action_table(s, "DecMaster");
+ }
+ return 1;
+}
+
+fn get_string_arg(state: *c.lua_State, idx: c_int) ?[]const u8 {
+ if (c.lua_type(state, idx) != c.LUA_TSTRING) return null;
+ return get_lua_string(state, idx);
+}
+
+fn get_lua_string(state: *c.lua_State, idx: c_int) ?[]const u8 {
+ const cstr = c.lua_tolstring(state, idx, null);
+ if (cstr == null) return null;
+ return std.mem.span(cstr);
+}
+
+fn parse_color(state: *c.lua_State, idx: c_int) u32 {
+ const lua_type = c.lua_type(state, idx);
+ if (lua_type == c.LUA_TNUMBER) {
+ return @intCast(c.lua_tointegerx(state, idx, null));
+ }
+ if (lua_type == c.LUA_TSTRING) {
+ const str = get_lua_string(state, idx) orelse return 0;
+ if (str.len > 0 and str[0] == '#') {
+ return std.fmt.parseInt(u32, str[1..], 16) catch return 0;
+ }
+ if (str.len > 2 and str[0] == '0' and str[1] == 'x') {
+ return std.fmt.parseInt(u32, str[2..], 16) catch return 0;
+ }
+ return std.fmt.parseInt(u32, str, 16) catch return 0;
+ }
+ return 0;
+}
+
+fn parse_scheme(state: *c.lua_State) Color_Scheme {
+ return Color_Scheme{
+ .fg = parse_color(state, 1),
+ .bg = parse_color(state, 2),
+ .border = parse_color(state, 3),
+ };
+}
+
+fn parse_modifiers(state: *c.lua_State, idx: c_int) u32 {
+ var mod_mask: u32 = 0;
+
+ if (c.lua_type(state, idx) != c.LUA_TTABLE) return mod_mask;
+
+ const len = c.lua_rawlen(state, idx);
+ var i: usize = 1;
+ while (i <= len) : (i += 1) {
+ _ = c.lua_rawgeti(state, idx, @intCast(i));
+ if (get_lua_string(state, -1)) |mod_str| {
+ mod_mask |= parse_single_modifier(mod_str);
+ }
+ c.lua_settop(state, -2);
+ }
+
+ return mod_mask;
+}
+
+fn parse_single_modifier(name: []const u8) u32 {
+ if (std.mem.eql(u8, name, "Mod4") or std.mem.eql(u8, name, "mod4") or std.mem.eql(u8, name, "super")) {
+ return (1 << 6);
+ } else if (std.mem.eql(u8, name, "Mod1") or std.mem.eql(u8, name, "mod1") or std.mem.eql(u8, name, "alt")) {
+ return (1 << 3);
+ } else if (std.mem.eql(u8, name, "Shift") or std.mem.eql(u8, name, "shift")) {
+ return (1 << 0);
+ } else if (std.mem.eql(u8, name, "Control") or std.mem.eql(u8, name, "control") or std.mem.eql(u8, name, "ctrl")) {
+ return (1 << 2);
+ }
+ return 0;
+}
+
+fn parse_action(name: []const u8) ?Action {
+ const action_map = .{
+ .{ "Spawn", Action.spawn },
+ .{ "SpawnTerminal", Action.spawn_terminal },
+ .{ "KillClient", Action.kill_client },
+ .{ "Quit", Action.quit },
+ .{ "Restart", Action.restart },
+ .{ "FocusStack", Action.focus_next },
+ .{ "MoveStack", Action.move_next },
+ .{ "ResizeMaster", Action.resize_master },
+ .{ "IncMaster", Action.inc_master },
+ .{ "DecMaster", Action.dec_master },
+ .{ "ToggleFloating", Action.toggle_floating },
+ .{ "ToggleFullScreen", Action.toggle_fullscreen },
+ .{ "ToggleGaps", Action.toggle_gaps },
+ .{ "CycleLayout", Action.cycle_layout },
+ .{ "ChangeLayout", Action.set_layout },
+ .{ "ViewTag", Action.view_tag },
+ .{ "ViewNextTag", Action.view_next_tag },
+ .{ "ViewPreviousTag", Action.view_prev_tag },
+ .{ "ViewNextNonEmptyTag", Action.view_next_nonempty_tag },
+ .{ "ViewPreviousNonEmptyTag", Action.view_prev_nonempty_tag },
+ .{ "MoveToTag", Action.move_to_tag },
+ .{ "ToggleView", Action.toggle_view_tag },
+ .{ "ToggleTag", Action.toggle_tag },
+ .{ "FocusMonitor", Action.focus_monitor },
+ .{ "TagMonitor", Action.send_to_monitor },
+ .{ "ScrollLeft", Action.scroll_left },
+ .{ "ScrollRight", Action.scroll_right },
+ };
+
+ inline for (action_map) |entry| {
+ if (std.mem.eql(u8, name, entry[0])) {
+ return entry[1];
+ }
+ }
+ return null;
+}
+
+fn key_name_to_keysym(name: []const u8) ?u64 {
+ const key_map = .{
+ .{ "Return", 0xff0d },
+ .{ "Enter", 0xff0d },
+ .{ "Tab", 0xff09 },
+ .{ "Escape", 0xff1b },
+ .{ "BackSpace", 0xff08 },
+ .{ "Delete", 0xffff },
+ .{ "space", 0x0020 },
+ .{ "Space", 0x0020 },
+ .{ "comma", 0x002c },
+ .{ "Comma", 0x002c },
+ .{ "period", 0x002e },
+ .{ "Period", 0x002e },
+ .{ "slash", 0x002f },
+ .{ "Slash", 0x002f },
+ .{ "minus", 0x002d },
+ .{ "Minus", 0x002d },
+ .{ "equal", 0x003d },
+ .{ "Equal", 0x003d },
+ .{ "bracketleft", 0x005b },
+ .{ "bracketright", 0x005d },
+ .{ "backslash", 0x005c },
+ .{ "semicolon", 0x003b },
+ .{ "apostrophe", 0x0027 },
+ .{ "grave", 0x0060 },
+ .{ "Left", 0xff51 },
+ .{ "Up", 0xff52 },
+ .{ "Right", 0xff53 },
+ .{ "Down", 0xff54 },
+ .{ "F1", 0xffbe },
+ .{ "F2", 0xffbf },
+ .{ "F3", 0xffc0 },
+ .{ "F4", 0xffc1 },
+ .{ "F5", 0xffc2 },
+ .{ "F6", 0xffc3 },
+ .{ "F7", 0xffc4 },
+ .{ "F8", 0xffc5 },
+ .{ "F9", 0xffc6 },
+ .{ "F10", 0xffc7 },
+ .{ "F11", 0xffc8 },
+ .{ "F12", 0xffc9 },
+ .{ "Print", 0xff61 },
+ .{ "XF86AudioRaiseVolume", 0x1008ff13 },
+ .{ "XF86AudioLowerVolume", 0x1008ff11 },
+ .{ "XF86AudioMute", 0x1008ff12 },
+ .{ "XF86AudioPlay", 0x1008ff14 },
+ .{ "XF86AudioPause", 0x1008ff31 },
+ .{ "XF86AudioNext", 0x1008ff17 },
+ .{ "XF86AudioPrev", 0x1008ff16 },
+ .{ "XF86MonBrightnessUp", 0x1008ff02 },
+ .{ "XF86MonBrightnessDown", 0x1008ff03 },
+ };
+
+ inline for (key_map) |entry| {
+ if (std.mem.eql(u8, name, entry[0])) {
+ return entry[1];
+ }
+ }
+
+ if (name.len == 1) {
+ const char = name[0];
+ if (char >= 'a' and char <= 'z') {
+ return char;
+ }
+ if (char >= 'A' and char <= 'Z') {
+ return char + 32;
+ }
+ if (char >= '0' and char <= '9') {
+ return char;
+ }
+ }
+
+ return null;
+}
diff --git a/src/config/lua_api.rs b/src/config/lua_api.rs
deleted file mode 100644
index 9685277..0000000
--- a/src/config/lua_api.rs
+++ /dev/null
@@ -1,1033 +0,0 @@
-use mlua::{Lua, Table, Value};
-use std::cell::RefCell;
-use std::rc::Rc;
-
-use crate::ColorScheme;
-use crate::bar::BlockConfig;
-use crate::errors::ConfigError;
-use crate::keyboard::handlers::{Arg, KeyAction, KeyBinding, KeyPress};
-use crate::keyboard::keysyms::{self, Keysym};
-use x11rb::protocol::xproto::KeyButMask;
-
-#[derive(Clone)]
-pub struct ConfigBuilder {
- pub border_width: u32,
- pub border_focused: u32,
- pub border_unfocused: u32,
- pub font: String,
- pub gaps_enabled: bool,
- pub smartgaps_enabled: bool,
- pub gap_inner_horizontal: u32,
- pub gap_inner_vertical: u32,
- pub gap_outer_horizontal: u32,
- pub gap_outer_vertical: u32,
- pub terminal: String,
- pub modkey: KeyButMask,
- pub tags: Vec<String>,
- pub layout_symbols: Vec<crate::LayoutSymbolOverride>,
- pub keybindings: Vec<KeyBinding>,
- pub tag_back_and_forth: bool,
- pub window_rules: Vec<crate::WindowRule>,
- pub status_blocks: Vec<BlockConfig>,
- pub scheme_normal: ColorScheme,
- pub scheme_occupied: ColorScheme,
- pub scheme_selected: ColorScheme,
- pub scheme_urgent: ColorScheme,
- pub autostart: Vec<String>,
- pub auto_tile: bool,
- pub hide_vacant_tags: bool,
-}
-
-impl Default for ConfigBuilder {
- fn default() -> Self {
- Self {
- border_width: 2,
- border_focused: 0x6dade3,
- border_unfocused: 0xbbbbbb,
- font: "monospace:style=Bold:size=10".to_string(),
- gaps_enabled: true,
- smartgaps_enabled: true,
- gap_inner_horizontal: 5,
- gap_inner_vertical: 5,
- gap_outer_horizontal: 5,
- gap_outer_vertical: 5,
- terminal: "st".to_string(),
- modkey: KeyButMask::MOD4,
- tags: vec!["1".into(), "2".into(), "3".into()],
- layout_symbols: Vec::new(),
- keybindings: Vec::new(),
- tag_back_and_forth: false,
- window_rules: Vec::new(),
- status_blocks: Vec::new(),
- scheme_normal: ColorScheme {
- foreground: 0xffffff,
- background: 0x000000,
- underline: 0x444444,
- },
- scheme_occupied: ColorScheme {
- foreground: 0xffffff,
- background: 0x000000,
- underline: 0x444444,
- },
- scheme_selected: ColorScheme {
- foreground: 0xffffff,
- background: 0x000000,
- underline: 0x444444,
- },
- scheme_urgent: ColorScheme {
- foreground: 0xff5555,
- background: 0x000000,
- underline: 0xff5555,
- },
- autostart: Vec::new(),
- auto_tile: false,
- hide_vacant_tags: false,
- }
- }
-}
-
-type SharedBuilder = Rc<RefCell<ConfigBuilder>>;
-
-pub fn register_api(lua: &Lua) -> Result<SharedBuilder, ConfigError> {
- let builder = Rc::new(RefCell::new(ConfigBuilder::default()));
-
- let oxwm_table = lua.create_table()?;
-
- register_spawn(lua, &oxwm_table, builder.clone())?;
- register_key_module(lua, &oxwm_table, builder.clone())?;
- register_gaps_module(lua, &oxwm_table, builder.clone())?;
- register_border_module(lua, &oxwm_table, builder.clone())?;
- register_client_module(lua, &oxwm_table)?;
- register_layout_module(lua, &oxwm_table)?;
- register_tag_module(lua, &oxwm_table, builder.clone())?;
- register_monitor_module(lua, &oxwm_table)?;
- register_rule_module(lua, &oxwm_table, builder.clone())?;
- register_bar_module(lua, &oxwm_table, builder.clone())?;
- register_misc(lua, &oxwm_table, builder.clone())?;
-
- lua.globals().set("oxwm", oxwm_table)?;
-
- Ok(builder)
-}
-
-fn register_spawn(lua: &Lua, parent: &Table, _builder: SharedBuilder) -> Result<(), ConfigError> {
- let spawn = lua.create_function(|lua, cmd: Value| create_action_table(lua, "Spawn", cmd))?;
- let spawn_terminal =
- lua.create_function(|lua, ()| create_action_table(lua, "SpawnTerminal", Value::Nil))?;
- parent.set("spawn", spawn)?;
- parent.set("spawn_terminal", spawn_terminal)?;
- Ok(())
-}
-
-fn register_key_module(
- lua: &Lua,
- parent: &Table,
- builder: SharedBuilder,
-) -> Result<(), ConfigError> {
- let key_table = lua.create_table()?;
-
- let builder_clone = builder.clone();
- let bind = lua.create_function(move |lua, (mods, key, action): (Value, String, Value)| {
- let modifiers = parse_modifiers_value(lua, mods)?;
- let keysym = parse_keysym(&key)?;
- let (key_action, arg) = parse_action_value(lua, action)?;
-
- let binding = KeyBinding::single_key(modifiers, keysym, key_action, arg);
- builder_clone.borrow_mut().keybindings.push(binding);
-
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let chord = lua.create_function(move |lua, (keys, action): (Table, Value)| {
- let mut key_presses = Vec::new();
-
- for i in 1..=keys.len()? {
- let key_spec: Table = keys.get(i)?;
- let mods: Value = key_spec.get(1)?;
- let key: String = key_spec.get(2)?;
-
- let modifiers = parse_modifiers_value(lua, mods)?;
- let keysym = parse_keysym(&key)?;
-
- key_presses.push(KeyPress { modifiers, keysym });
- }
-
- let (key_action, arg) = parse_action_value(lua, action)?;
- let binding = KeyBinding::new(key_presses, key_action, arg);
- builder_clone.borrow_mut().keybindings.push(binding);
-
- Ok(())
- })?;
-
- key_table.set("bind", bind)?;
- key_table.set("chord", chord)?;
- parent.set("key", key_table)?;
- Ok(())
-}
-
-fn register_gaps_module(
- lua: &Lua,
- parent: &Table,
- builder: SharedBuilder,
-) -> Result<(), ConfigError> {
- let gaps_table = lua.create_table()?;
-
- let builder_clone = builder.clone();
- let set_enabled = lua.create_function(move |_, enabled: bool| {
- builder_clone.borrow_mut().gaps_enabled = enabled;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let enable = lua.create_function(move |_, ()| {
- builder_clone.borrow_mut().gaps_enabled = true;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let disable = lua.create_function(move |_, ()| {
- builder_clone.borrow_mut().gaps_enabled = false;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_inner = lua.create_function(move |_, (h, v): (u32, u32)| {
- let mut b = builder_clone.borrow_mut();
- b.gap_inner_horizontal = h;
- b.gap_inner_vertical = v;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_outer = lua.create_function(move |_, (h, v): (u32, u32)| {
- let mut b = builder_clone.borrow_mut();
- b.gap_outer_horizontal = h;
- b.gap_outer_vertical = v;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_smart = lua.create_function(move |_, enabled: bool| {
- builder_clone.borrow_mut().smartgaps_enabled = enabled;
- Ok(())
- })?;
-
- gaps_table.set("set_enabled", set_enabled)?;
- gaps_table.set("enable", enable)?;
- gaps_table.set("disable", disable)?;
- gaps_table.set("set_inner", set_inner)?;
- gaps_table.set("set_outer", set_outer)?;
- gaps_table.set("set_smart", set_smart)?;
- parent.set("gaps", gaps_table)?;
- Ok(())
-}
-
-fn register_border_module(
- lua: &Lua,
- parent: &Table,
- builder: SharedBuilder,
-) -> Result<(), ConfigError> {
- let border_table = lua.create_table()?;
-
- let builder_clone = builder.clone();
- let set_width = lua.create_function(move |_, width: u32| {
- builder_clone.borrow_mut().border_width = width;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_focused_color = lua.create_function(move |_, color: Value| {
- let color_u32 = parse_color_value(color)?;
- builder_clone.borrow_mut().border_focused = color_u32;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_unfocused_color = lua.create_function(move |_, color: Value| {
- let color_u32 = parse_color_value(color)?;
- builder_clone.borrow_mut().border_unfocused = color_u32;
- Ok(())
- })?;
-
- border_table.set("set_width", set_width)?;
- border_table.set("set_focused_color", set_focused_color)?;
- border_table.set("set_unfocused_color", set_unfocused_color)?;
- parent.set("border", border_table)?;
- Ok(())
-}
-
-fn register_client_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
- let client_table = lua.create_table()?;
-
- let kill = lua.create_function(|lua, ()| create_action_table(lua, "KillClient", Value::Nil))?;
-
- let toggle_fullscreen =
- lua.create_function(|lua, ()| create_action_table(lua, "ToggleFullScreen", Value::Nil))?;
-
- let toggle_floating =
- lua.create_function(|lua, ()| create_action_table(lua, "ToggleFloating", Value::Nil))?;
-
- let focus_stack = lua.create_function(|lua, dir: i32| {
- create_action_table(lua, "FocusStack", Value::Integer(dir as i64))
- })?;
-
- let move_stack = lua.create_function(|lua, dir: i32| {
- create_action_table(lua, "MoveStack", Value::Integer(dir as i64))
- })?;
-
- client_table.set("kill", kill)?;
- client_table.set("toggle_fullscreen", toggle_fullscreen)?;
- client_table.set("toggle_floating", toggle_floating)?;
- client_table.set("focus_stack", focus_stack)?;
- client_table.set("move_stack", move_stack)?;
-
- parent.set("client", client_table)?;
- Ok(())
-}
-
-fn register_layout_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
- let layout_table = lua.create_table()?;
-
- let cycle =
- lua.create_function(|lua, ()| create_action_table(lua, "CycleLayout", Value::Nil))?;
-
- let set = lua.create_function(|lua, name: String| {
- create_action_table(
- lua,
- "ChangeLayout",
- Value::String(lua.create_string(&name)?),
- )
- })?;
-
- let scroll_left =
- lua.create_function(|lua, ()| create_action_table(lua, "ScrollLeft", Value::Nil))?;
-
- let scroll_right =
- lua.create_function(|lua, ()| create_action_table(lua, "ScrollRight", Value::Nil))?;
-
- layout_table.set("cycle", cycle)?;
- layout_table.set("set", set)?;
- layout_table.set("scroll_left", scroll_left)?;
- layout_table.set("scroll_right", scroll_right)?;
- parent.set("layout", layout_table)?;
- Ok(())
-}
-
-fn register_tag_module(
- lua: &Lua,
- parent: &Table,
- builder: SharedBuilder,
-) -> Result<(), ConfigError> {
- let builder_clone = builder.clone();
- let tag_table = lua.create_table()?;
-
- let view = lua.create_function(|lua, idx: i32| {
- create_action_table(lua, "ViewTag", Value::Integer(idx as i64))
- })?;
-
- let view_next =
- lua.create_function(|lua, ()| create_action_table(lua, "ViewNextTag", Value::Nil))?;
-
- let view_previous =
- lua.create_function(|lua, ()| create_action_table(lua, "ViewPreviousTag", Value::Nil))?;
-
- let view_next_nonempty =
- lua.create_function(|lua, ()| create_action_table(lua, "ViewNextNonEmptyTag", Value::Nil))?;
-
- let view_previous_nonempty = lua.create_function(|lua, ()| {
- create_action_table(lua, "ViewPreviousNonEmptyTag", Value::Nil)
- })?;
-
- let toggleview = lua.create_function(|lua, idx: i32| {
- create_action_table(lua, "ToggleView", Value::Integer(idx as i64))
- })?;
-
- let move_to = lua.create_function(|lua, idx: i32| {
- create_action_table(lua, "MoveToTag", Value::Integer(idx as i64))
- })?;
-
- let toggletag = lua.create_function(|lua, idx: i32| {
- create_action_table(lua, "ToggleTag", Value::Integer(idx as i64))
- })?;
-
- let set_back_and_forth = lua.create_function(move |_, enabled: bool| {
- builder_clone.borrow_mut().tag_back_and_forth = enabled;
- Ok(())
- })?;
-
- tag_table.set("view", view)?;
- tag_table.set("view_next", view_next)?;
- tag_table.set("view_previous", view_previous)?;
- tag_table.set("view_next_nonempty", view_next_nonempty)?;
- tag_table.set("view_previous_nonempty", view_previous_nonempty)?;
- tag_table.set("toggleview", toggleview)?;
- tag_table.set("move_to", move_to)?;
- tag_table.set("toggletag", toggletag)?;
- tag_table.set("set_back_and_forth", set_back_and_forth)?;
- parent.set("tag", tag_table)?;
- Ok(())
-}
-
-fn register_monitor_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError> {
- let monitor_table = lua.create_table()?;
-
- let focus = lua.create_function(|lua, direction: i64| {
- create_action_table(lua, "FocusMonitor", Value::Integer(direction))
- })?;
-
- let tag = lua.create_function(|lua, direction: i64| {
- create_action_table(lua, "TagMonitor", Value::Integer(direction))
- })?;
-
- monitor_table.set("focus", focus)?;
- monitor_table.set("tag", tag)?;
- parent.set("monitor", monitor_table)?;
- Ok(())
-}
-
-fn register_rule_module(
- lua: &Lua,
- parent: &Table,
- builder: SharedBuilder,
-) -> Result<(), ConfigError> {
- let rule_table = lua.create_table()?;
-
- let builder_clone = builder.clone();
- let add = lua.create_function(move |_, config: Table| {
- let class: Option<String> = config.get("class").ok();
- let instance: Option<String> = config.get("instance").ok();
- let title: Option<String> = config.get("title").ok();
- let is_floating: Option<bool> = config.get("floating").ok();
- let monitor: Option<usize> = config.get("monitor").ok();
- let focus: Option<bool> = config.get("focus").ok();
-
- let tags: Option<u32> = if let Ok(tag_index) = config.get::<i32>("tag") {
- if tag_index > 0 {
- Some(1 << (tag_index - 1))
- } else {
- None
- }
- } else {
- None
- };
-
- let rule = crate::WindowRule {
- class,
- instance,
- title,
- tags,
- focus,
- is_floating,
- monitor,
- };
-
- builder_clone.borrow_mut().window_rules.push(rule);
- Ok(())
- })?;
-
- rule_table.set("add", add)?;
- parent.set("rule", rule_table)?;
- Ok(())
-}
-
-fn register_bar_module(
- lua: &Lua,
- parent: &Table,
- builder: SharedBuilder,
-) -> Result<(), ConfigError> {
- let bar_table = lua.create_table()?;
-
- let builder_clone = builder.clone();
- let set_font = lua.create_function(move |_, font: String| {
- builder_clone.borrow_mut().font = font;
- Ok(())
- })?;
-
- let block_table = lua.create_table()?;
-
- let ram =
- lua.create_function(|lua, config: Table| create_block_config(lua, config, "Ram", None))?;
-
- let datetime = lua.create_function(|lua, config: Table| {
- let date_format: String = config.get("date_format").map_err(|_| {
- mlua::Error::RuntimeError(
- "oxwm.bar.block.datetime: 'date_format' field is required (e.g., '%H:%M')".into(),
- )
- })?;
- create_block_config(
- lua,
- config,
- "DateTime",
- Some(Value::String(lua.create_string(&date_format)?)),
- )
- })?;
-
- let shell = lua.create_function(|lua, config: Table| {
- let command: String = config.get("command").map_err(|_| {
- mlua::Error::RuntimeError("oxwm.bar.block.shell: 'command' field is required".into())
- })?;
- create_block_config(
- lua,
- config,
- "Shell",
- Some(Value::String(lua.create_string(&command)?)),
- )
- })?;
-
- let static_block = lua.create_function(|lua, config: Table| {
- let text: String = config.get("text").map_err(|_| {
- mlua::Error::RuntimeError("oxwm.bar.block.static: 'text' field is required".into())
- })?;
- create_block_config(
- lua,
- config,
- "Static",
- Some(Value::String(lua.create_string(&text)?)),
- )
- })?;
-
- let battery = lua.create_function(|lua, config: Table| {
- let charging: String = config.get("charging").map_err(|_| {
- mlua::Error::RuntimeError("oxwm.bar.block.battery: 'charging' field is required".into())
- })?;
- let discharging: String = config.get("discharging").map_err(|_| {
- mlua::Error::RuntimeError(
- "oxwm.bar.block.battery: 'discharging' field is required".into(),
- )
- })?;
- let full: String = config.get("full").map_err(|_| {
- mlua::Error::RuntimeError("oxwm.bar.block.battery: 'full' field is required".into())
- })?;
- let battery_name: Option<String> = config.get("battery_name").unwrap_or(None);
-
- let formats_table = lua.create_table()?;
- formats_table.set("charging", charging)?;
- formats_table.set("discharging", discharging)?;
- formats_table.set("full", full)?;
- formats_table.set("battery_name", battery_name)?;
-
- create_block_config(lua, config, "Battery", Some(Value::Table(formats_table)))
- })?;
-
- block_table.set("ram", ram)?;
- block_table.set("datetime", datetime)?;
- block_table.set("shell", shell)?;
- block_table.set("static", static_block)?;
- block_table.set("battery", battery)?;
-
- // Deprecated add_block() function for backwards compatibility
- // This allows old configs to still work, but users should migrate to set_blocks()
- let builder_clone = builder.clone();
- let add_block = lua.create_function(move |_, (format, block_type, arg, interval, color, underline): (String, String, Value, u64, Value, Option<bool>)| -> mlua::Result<()> {
- eprintln!("WARNING: oxwm.bar.add_block() is deprecated. Please migrate to oxwm.bar.set_blocks() with block constructors.");
- eprintln!("See the migration guide for details.");
-
- let cmd = match block_type.as_str() {
- "DateTime" => {
- let fmt = if let Value::String(s) = arg {
- s.to_str()?.to_string()
- } else {
- return Err(mlua::Error::RuntimeError("DateTime block requires format string as third argument".into()));
- };
- crate::bar::BlockCommand::DateTime(fmt)
- }
- "Shell" => {
- let cmd_str = if let Value::String(s) = arg {
- s.to_str()?.to_string()
- } else {
- return Err(mlua::Error::RuntimeError("Shell block requires command string as third argument".into()));
- };
- crate::bar::BlockCommand::Shell(cmd_str)
- }
- "Ram" => crate::bar::BlockCommand::Ram,
- "Static" => {
- let text = if let Value::String(s) = arg {
- s.to_str()?.to_string()
- } else {
- String::new()
- };
- crate::bar::BlockCommand::Static(text)
- }
- "Battery" => {
- return Err(mlua::Error::RuntimeError(
- "Battery block is not supported with add_block(). Please use oxwm.bar.set_blocks() with oxwm.bar.block.battery()".into()
- ));
- }
- _ => return Err(mlua::Error::RuntimeError(format!("Unknown block type '{}'", block_type))),
- };
-
- let color_u32 = parse_color_value(color)?;
-
- let block = crate::bar::BlockConfig {
- format,
- command: cmd,
- interval_secs: interval,
- color: color_u32,
- underline: underline.unwrap_or(false),
- };
-
- builder_clone.borrow_mut().status_blocks.push(block);
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_blocks = lua.create_function(move |_, blocks: Table| {
- use crate::bar::BlockCommand;
-
- let mut block_configs = Vec::new();
-
- for i in 1..=blocks.len()? {
- let block_table: Table = blocks.get(i)?;
- let block_type: String = block_table.get("__block_type")?;
- let format: String = block_table.get("format").unwrap_or_default();
- let interval: u64 = block_table.get("interval")?;
- let color_val: Value = block_table.get("color")?;
- let underline: bool = block_table.get("underline").unwrap_or(false);
- let arg: Option<Value> = block_table.get("__arg").ok();
-
- let cmd = match block_type.as_str() {
- "DateTime" => {
- let fmt = arg
- .and_then(|v| {
- if let Value::String(s) = v {
- s.to_str().ok().map(|s| s.to_string())
- } else {
- None
- }
- })
- .ok_or_else(|| {
- mlua::Error::RuntimeError("DateTime block missing format".into())
- })?;
- BlockCommand::DateTime(fmt)
- }
- "Shell" => {
- let cmd_str = arg
- .and_then(|v| {
- if let Value::String(s) = v {
- s.to_str().ok().map(|s| s.to_string())
- } else {
- None
- }
- })
- .ok_or_else(|| {
- mlua::Error::RuntimeError("Shell block missing command".into())
- })?;
- BlockCommand::Shell(cmd_str)
- }
- "Ram" => BlockCommand::Ram,
- "Static" => {
- let text = arg
- .and_then(|v| {
- if let Value::String(s) = v {
- s.to_str().ok().map(|s| s.to_string())
- } else {
- None
- }
- })
- .unwrap_or_default();
- BlockCommand::Static(text)
- }
- "Battery" => {
- let formats = arg
- .and_then(|v| {
- if let Value::Table(t) = v {
- Some(t)
- } else {
- None
- }
- })
- .ok_or_else(|| {
- mlua::Error::RuntimeError("Battery block missing formats".into())
- })?;
-
- let charging: String = formats.get("charging")?;
- let discharging: String = formats.get("discharging")?;
- let full: String = formats.get("full")?;
- let battery_name: Option<String> = formats.get("battery_name").unwrap_or(None);
-
- BlockCommand::Battery {
- format_charging: charging,
- format_discharging: discharging,
- format_full: full,
- battery_name,
- }
- }
- _ => {
- return Err(mlua::Error::RuntimeError(format!(
- "Unknown block type '{}'",
- block_type
- )));
- }
- };
-
- let color_u32 = parse_color_value(color_val)?;
-
- let block = crate::bar::BlockConfig {
- format,
- command: cmd,
- interval_secs: interval,
- color: color_u32,
- underline,
- };
-
- block_configs.push(block);
- }
-
- builder_clone.borrow_mut().status_blocks = block_configs;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_scheme_normal =
- lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
- let foreground = parse_color_value(fg)?;
- let background = parse_color_value(bg)?;
- let underline = parse_color_value(ul)?;
-
- builder_clone.borrow_mut().scheme_normal = ColorScheme {
- foreground,
- background,
- underline,
- };
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_scheme_occupied =
- lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
- let foreground = parse_color_value(fg)?;
- let background = parse_color_value(bg)?;
- let underline = parse_color_value(ul)?;
-
- builder_clone.borrow_mut().scheme_occupied = ColorScheme {
- foreground,
- background,
- underline,
- };
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_scheme_selected =
- lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
- let foreground = parse_color_value(fg)?;
- let background = parse_color_value(bg)?;
- let underline = parse_color_value(ul)?;
-
- builder_clone.borrow_mut().scheme_selected = ColorScheme {
- foreground,
- background,
- underline,
- };
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_scheme_urgent =
- lua.create_function(move |_, (fg, bg, ul): (Value, Value, Value)| {
- let foreground = parse_color_value(fg)?;
- let background = parse_color_value(bg)?;
- let underline = parse_color_value(ul)?;
-
- builder_clone.borrow_mut().scheme_urgent = ColorScheme {
- foreground,
- background,
- underline,
- };
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_hide_vacant_tags = lua.create_function(move |_, hide: bool| {
- builder_clone.borrow_mut().hide_vacant_tags = hide;
- Ok(())
- })?;
-
- bar_table.set("set_font", set_font)?;
- bar_table.set("block", block_table)?;
- bar_table.set("add_block", add_block)?; // Deprecated, for backwards compatibility
- bar_table.set("set_blocks", set_blocks)?;
- bar_table.set("set_scheme_normal", set_scheme_normal)?;
- bar_table.set("set_scheme_occupied", set_scheme_occupied)?;
- bar_table.set("set_scheme_selected", set_scheme_selected)?;
- bar_table.set("set_scheme_urgent", set_scheme_urgent)?;
- bar_table.set("set_hide_vacant_tags", set_hide_vacant_tags)?;
- parent.set("bar", bar_table)?;
- Ok(())
-}
-
-fn register_misc(lua: &Lua, parent: &Table, builder: SharedBuilder) -> Result<(), ConfigError> {
- let builder_clone = builder.clone();
- let set_terminal = lua.create_function(move |_, term: String| {
- builder_clone.borrow_mut().terminal = term;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_modkey = lua.create_function(move |_, modkey_str: String| {
- let modkey = parse_modkey_string(&modkey_str)
- .map_err(|e| mlua::Error::RuntimeError(format!("{}", e)))?;
- builder_clone.borrow_mut().modkey = modkey;
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let set_tags = lua.create_function(move |_, tags: Vec<String>| {
- builder_clone.borrow_mut().tags = tags;
- Ok(())
- })?;
-
- let quit = lua.create_function(|lua, ()| create_action_table(lua, "Quit", Value::Nil))?;
-
- let restart = lua.create_function(|lua, ()| create_action_table(lua, "Restart", Value::Nil))?;
-
- let toggle_gaps =
- lua.create_function(|lua, ()| create_action_table(lua, "ToggleGaps", Value::Nil))?;
-
- let set_master_factor = lua.create_function(|lua, delta: i32| {
- create_action_table(lua, "SetMasterFactor", Value::Integer(delta as i64))
- })?;
-
- let inc_num_master = lua.create_function(|lua, delta: i32| {
- create_action_table(lua, "IncNumMaster", Value::Integer(delta as i64))
- })?;
-
- let show_keybinds =
- lua.create_function(|lua, ()| create_action_table(lua, "ShowKeybindOverlay", Value::Nil))?;
-
- let focus_monitor = lua.create_function(|lua, idx: i32| {
- create_action_table(lua, "FocusMonitor", Value::Integer(idx as i64))
- })?;
-
- let builder_clone = builder.clone();
- let set_layout_symbol = lua.create_function(move |_, (name, symbol): (String, String)| {
- builder_clone
- .borrow_mut()
- .layout_symbols
- .push(crate::LayoutSymbolOverride { name, symbol });
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let autostart = lua.create_function(move |_, cmd: String| {
- builder_clone.borrow_mut().autostart.push(cmd);
- Ok(())
- })?;
-
- let builder_clone = builder.clone();
- let auto_tile = lua.create_function(move |_, enabled: bool| {
- builder_clone.borrow_mut().auto_tile = enabled;
- Ok(())
- })?;
-
- parent.set("set_terminal", set_terminal)?;
- parent.set("set_modkey", set_modkey)?;
- parent.set("set_tags", set_tags)?;
- parent.set("set_layout_symbol", set_layout_symbol)?;
- parent.set("autostart", autostart)?;
- parent.set("quit", quit)?;
- parent.set("restart", restart)?;
- parent.set("toggle_gaps", toggle_gaps)?;
- parent.set("set_master_factor", set_master_factor)?;
- parent.set("inc_num_master", inc_num_master)?;
- parent.set("show_keybinds", show_keybinds)?;
- parent.set("focus_monitor", focus_monitor)?;
- parent.set("auto_tile", auto_tile)?;
- Ok(())
-}
-
-fn parse_modifiers_value(_lua: &Lua, value: Value) -> mlua::Result<Vec<KeyButMask>> {
- match value {
- Value::Table(t) => {
- let mut mods = Vec::new();
- for i in 1..=t.len()? {
- let mod_str: String = t.get(i)?;
- let mask = parse_modkey_string(&mod_str)
- .map_err(|e| mlua::Error::RuntimeError(format!("oxwm.key.bind: invalid modifier - {}", e)))?;
- mods.push(mask);
- }
- Ok(mods)
- }
- Value::String(s) => {
- let s_str = s.to_str()?;
- let mask = parse_modkey_string(&s_str)
- .map_err(|e| mlua::Error::RuntimeError(format!("oxwm.key.bind: invalid modifier - {}", e)))?;
- Ok(vec![mask])
- }
- _ => Err(mlua::Error::RuntimeError(
- "oxwm.key.bind: first argument must be a table of modifiers like {\"Mod4\"} or {\"Mod4\", \"Shift\"}".into(),
- )),
- }
-}
-
-fn parse_modkey_string(s: &str) -> Result<KeyButMask, ConfigError> {
- match s {
- "Mod1" => Ok(KeyButMask::MOD1),
- "Mod2" => Ok(KeyButMask::MOD2),
- "Mod3" => Ok(KeyButMask::MOD3),
- "Mod4" => Ok(KeyButMask::MOD4),
- "Mod5" => Ok(KeyButMask::MOD5),
- "Shift" => Ok(KeyButMask::SHIFT),
- "Control" => Ok(KeyButMask::CONTROL),
- _ => Err(ConfigError::InvalidModkey(format!(
- "'{}' is not a valid modifier. Use one of: Mod1, Mod4, Shift, Control",
- s
- ))),
- }
-}
-
-fn parse_keysym(key: &str) -> mlua::Result<Keysym> {
- keysyms::keysym_from_str(key)
- .ok_or_else(|| mlua::Error::RuntimeError(format!("unknown key '{}'. valid keys include: Return, Space, A-Z, 0-9, F1-F12, Left, Right, Up, Down, etc. check oxwm.lua type definitions for the complete list", key)))
-}
-
-fn parse_action_value(_lua: &Lua, value: Value) -> mlua::Result<(KeyAction, Arg)> {
- match value {
- Value::Function(_) => {
- Err(mlua::Error::RuntimeError(
- "action must be a function call, not a function reference. did you forget ()? example: oxwm.spawn('st') not oxwm.spawn".into()
- ))
- }
- Value::Table(t) => {
- if let Ok(action_name) = t.get::<String>("__action") {
- let action = string_to_action(&action_name)?;
- let arg = if let Ok(arg_val) = t.get::<Value>("__arg") {
- value_to_arg(arg_val)?
- } else {
- Arg::None
- };
- return Ok((action, arg));
- }
-
- Err(mlua::Error::RuntimeError(
- "action must be a table returned by oxwm functions like oxwm.spawn(), oxwm.client.kill(), oxwm.quit(), etc.".into(),
- ))
- }
- _ => Err(mlua::Error::RuntimeError(
- "action must be a table returned by oxwm functions like oxwm.spawn(), oxwm.client.kill(), oxwm.quit(), etc.".into(),
- )),
- }
-}
-
-fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
- match s {
- "Spawn" => Ok(KeyAction::Spawn),
- "SpawnTerminal" => Ok(KeyAction::SpawnTerminal),
- "KillClient" => Ok(KeyAction::KillClient),
- "FocusStack" => Ok(KeyAction::FocusStack),
- "MoveStack" => Ok(KeyAction::MoveStack),
- "Quit" => Ok(KeyAction::Quit),
- "Restart" => Ok(KeyAction::Restart),
- "ViewTag" => Ok(KeyAction::ViewTag),
- "ViewNextTag" => Ok(KeyAction::ViewNextTag),
- "ViewPreviousTag" => Ok(KeyAction::ViewPreviousTag),
- "ViewNextNonEmptyTag" => Ok(KeyAction::ViewNextNonEmptyTag),
- "ViewPreviousNonEmptyTag" => Ok(KeyAction::ViewPreviousNonEmptyTag),
- "ToggleView" => Ok(KeyAction::ToggleView),
- "MoveToTag" => Ok(KeyAction::MoveToTag),
- "ToggleTag" => Ok(KeyAction::ToggleTag),
- "ToggleGaps" => Ok(KeyAction::ToggleGaps),
- "SetMasterFactor" => Ok(KeyAction::SetMasterFactor),
- "IncNumMaster" => Ok(KeyAction::IncNumMaster),
- "ToggleFullScreen" => Ok(KeyAction::ToggleFullScreen),
- "ToggleFloating" => Ok(KeyAction::ToggleFloating),
- "ChangeLayout" => Ok(KeyAction::ChangeLayout),
- "CycleLayout" => Ok(KeyAction::CycleLayout),
- "FocusMonitor" => Ok(KeyAction::FocusMonitor),
- "TagMonitor" => Ok(KeyAction::TagMonitor),
- "ShowKeybindOverlay" => Ok(KeyAction::ShowKeybindOverlay),
- "ScrollLeft" => Ok(KeyAction::ScrollLeft),
- "ScrollRight" => Ok(KeyAction::ScrollRight),
- _ => Err(mlua::Error::RuntimeError(format!(
- "unknown action '{}'. this is an internal error, please report it",
- s
- ))),
- }
-}
-
-fn value_to_arg(value: Value) -> mlua::Result<Arg> {
- match value {
- Value::Nil => Ok(Arg::None),
- Value::String(s) => Ok(Arg::Str(s.to_str()?.to_string())),
- Value::Integer(i) => Ok(Arg::Int(i as i32)),
- Value::Number(n) => Ok(Arg::Int(n as i32)),
- Value::Table(t) => {
- let mut arr = Vec::new();
- for i in 1..=t.len()? {
- let item: String = t.get(i)?;
- arr.push(item);
- }
- Ok(Arg::Array(arr))
- }
- _ => Ok(Arg::None),
- }
-}
-
-fn create_action_table(lua: &Lua, action_name: &str, arg: Value) -> mlua::Result<Table> {
- let table = lua.create_table()?;
- table.set("__action", action_name)?;
- table.set("__arg", arg)?;
- Ok(table)
-}
-
-fn parse_color_value(value: Value) -> mlua::Result<u32> {
- match value {
- Value::Integer(i) => Ok(i as u32),
- Value::Number(n) => Ok(n as u32),
- Value::String(s) => {
- let s = s.to_str()?;
- if let Some(hex) = s.strip_prefix('#') {
- u32::from_str_radix(hex, 16).map_err(|e| {
- mlua::Error::RuntimeError(format!(
- "invalid hex color '{}': {}. use format like #ff0000 or 0xff0000",
- s, e
- ))
- })
- } else if let Some(hex) = s.strip_prefix("0x") {
- u32::from_str_radix(hex, 16).map_err(|e| {
- mlua::Error::RuntimeError(format!(
- "invalid hex color '{}': {}. use format like 0xff0000 or #ff0000",
- s, e
- ))
- })
- } else {
- s.parse::<u32>().map_err(|e| {
- mlua::Error::RuntimeError(format!(
- "invalid color '{}': {}. use hex format like 0xff0000 or #ff0000",
- s, e
- ))
- })
- }
- }
- _ => Err(mlua::Error::RuntimeError(
- "color must be a number (0xff0000) or string ('#ff0000' or '0xff0000')".into(),
- )),
- }
-}
-
-fn create_block_config(
- lua: &Lua,
- config: Table,
- block_type: &str,
- arg: Option<Value>,
-) -> mlua::Result<Table> {
- let table = lua.create_table()?;
- table.set("__block_type", block_type)?;
-
- let format: String = config.get("format").unwrap_or_default();
- let interval: u64 = config.get("interval")?;
- let color: Value = config.get("color")?;
- let underline: bool = config.get("underline").unwrap_or(false);
-
- table.set("format", format)?;
- table.set("interval", interval)?;
- table.set("color", color)?;
- table.set("underline", underline)?;
-
- if let Some(arg_val) = arg {
- table.set("__arg", arg_val)?;
- }
-
- Ok(table)
-}
diff --git a/src/config/mod.rs b/src/config/mod.rs
deleted file mode 100644
index 4628e1a..0000000
--- a/src/config/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-mod lua;
-mod lua_api;
-
-pub use lua::parse_lua_config;
diff --git a/src/errors.rs b/src/errors.rs
deleted file mode 100644
index fbba5db..0000000
--- a/src/errors.rs
+++ /dev/null
@@ -1,221 +0,0 @@
-use std::io;
-
-#[derive(Debug)]
-pub enum WmError {
- X11(X11Error),
- Io(io::Error),
- Config(ConfigError),
- Block(BlockError),
- Autostart(String, io::Error),
-}
-
-#[derive(Debug)]
-pub enum X11Error {
- ConnectError(x11rb::errors::ConnectError),
- ConnectionError(x11rb::errors::ConnectionError),
- ReplyError(x11rb::errors::ReplyError),
- ReplyOrIdError(x11rb::errors::ReplyOrIdError),
- DisplayOpenFailed,
- FontLoadFailed(String),
- DrawCreateFailed,
-}
-
-#[derive(Debug)]
-pub enum ConfigError {
- LuaError(String),
- InvalidModkey(String),
- UnknownKey(String),
- UnknownAction(String),
- UnknownBlockCommand(String),
- MissingCommandArg { command: String, field: String },
- ValidationError(String),
- NoConfigPathSet,
- NoConfigAtPath,
- CouldNotReadConfig(std::io::Error),
-}
-
-#[derive(Debug)]
-pub enum BlockError {
- Io(io::Error),
- ParseInt(std::num::ParseIntError),
- MissingFile(String),
- InvalidData(String),
- CommandFailed(String),
-}
-
-pub enum MainError {
- CouldNotCreateConfigDir(std::io::Error),
- CouldNotWriteConfig(std::io::Error),
- FailedCheckExist(std::io::Error),
- FailedReadConfig(std::io::Error),
- FailedReadConfigTemplate(ConfigError),
- CouldNotStartWm(WmError),
- WmError(WmError),
- BadConfigPath,
- NoConfigPath,
- InvalidArguments,
- NoProgramName,
- NoConfigDir,
-}
-
-impl std::fmt::Display for WmError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::X11(error) => write!(f, "{}", error),
- Self::Io(error) => write!(f, "{}", error),
- Self::Config(error) => write!(f, "{}", error),
- Self::Block(error) => write!(f, "{}", error),
- Self::Autostart(command, error) => write!(
- f,
- "Failed to spawn autostart command '{}': {}",
- command, error
- ),
- }
- }
-}
-
-impl std::fmt::Display for X11Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::ConnectError(err) => write!(f, "{}", err),
- Self::ConnectionError(err) => write!(f, "{}", err),
- Self::ReplyError(err) => write!(f, "{}", err),
- Self::ReplyOrIdError(err) => write!(f, "{}", err),
- Self::DisplayOpenFailed => write!(f, "failed to open X11 display"),
- Self::FontLoadFailed(font_name) => write!(f, "failed to load Xft font: {}", font_name),
- Self::DrawCreateFailed => write!(f, "failed to create XftDraw"),
- }
- }
-}
-
-impl std::fmt::Display for ConfigError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::LuaError(msg) => write!(f, "{}", msg),
- Self::InvalidModkey(msg) => write!(f, "{}", msg),
- Self::UnknownKey(msg) => write!(f, "{}", msg),
- Self::UnknownAction(msg) => write!(f, "{}", msg),
- Self::UnknownBlockCommand(msg) => write!(f, "{}", msg),
- Self::MissingCommandArg { command, field } => {
- write!(f, "{} command requires {}", command, field)
- }
- Self::ValidationError(msg) => write!(f, "{}", msg),
- Self::NoConfigPathSet => write!(
- f,
- "Could not find config file. Config path should've been set while loading"
- ),
- Self::NoConfigAtPath => write!(f, "Could not find config file, has it been moved?"),
- Self::CouldNotReadConfig(e) => write!(f, "Could not read config: {e}"),
- }
- }
-}
-
-impl std::fmt::Display for BlockError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Io(err) => write!(f, "Block I/O error: {}", err),
- Self::ParseInt(err) => write!(f, "Block parse error: {}", err),
- Self::MissingFile(path) => write!(f, "Block missing file: {}", path),
- Self::InvalidData(msg) => write!(f, "Block invalid data: {}", msg),
- Self::CommandFailed(msg) => write!(f, "Block command failed: {}", msg),
- }
- }
-}
-
-impl<T: Into<X11Error>> From<T> for WmError {
- fn from(value: T) -> Self {
- Self::X11(value.into())
- }
-}
-
-impl From<io::Error> for WmError {
- fn from(value: io::Error) -> Self {
- Self::Io(value)
- }
-}
-
-impl From<ConfigError> for WmError {
- fn from(value: ConfigError) -> Self {
- Self::Config(value)
- }
-}
-
-impl From<BlockError> for WmError {
- fn from(value: BlockError) -> Self {
- Self::Block(value)
- }
-}
-
-impl From<io::Error> for BlockError {
- fn from(value: io::Error) -> Self {
- BlockError::Io(value)
- }
-}
-
-impl From<std::num::ParseIntError> for BlockError {
- fn from(value: std::num::ParseIntError) -> Self {
- BlockError::ParseInt(value)
- }
-}
-
-impl From<x11rb::errors::ConnectError> for X11Error {
- fn from(value: x11rb::errors::ConnectError) -> Self {
- X11Error::ConnectError(value)
- }
-}
-
-impl From<x11rb::errors::ConnectionError> for X11Error {
- fn from(value: x11rb::errors::ConnectionError) -> Self {
- X11Error::ConnectionError(value)
- }
-}
-
-impl From<x11rb::errors::ReplyError> for X11Error {
- fn from(value: x11rb::errors::ReplyError) -> Self {
- X11Error::ReplyError(value)
- }
-}
-
-impl From<x11rb::errors::ReplyOrIdError> for X11Error {
- fn from(value: x11rb::errors::ReplyOrIdError) -> Self {
- X11Error::ReplyOrIdError(value)
- }
-}
-
-impl From<mlua::Error> for ConfigError {
- fn from(err: mlua::Error) -> Self {
- ConfigError::LuaError(err.to_string())
- }
-}
-
-pub trait LuaResultExt<T> {
- fn lua_context(self, context: &str) -> Result<T, ConfigError>;
-}
-
-impl<T> LuaResultExt<T> for Result<T, mlua::Error> {
- fn lua_context(self, context: &str) -> Result<T, ConfigError> {
- self.map_err(|e| ConfigError::LuaError(format!("{}: {}", context, e)))
- }
-}
-
-impl std::fmt::Debug for MainError {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- use MainError::*;
-
- match self {
- CouldNotCreateConfigDir(e)
- | CouldNotWriteConfig(e)
- | FailedCheckExist(e)
- | FailedReadConfig(e) => {
- write!(f, "{e}")
- }
- FailedReadConfigTemplate(e) => write!(f, "{e}"),
- CouldNotStartWm(e) | WmError(e) => write!(f, "{e}"),
- BadConfigPath => write!(f, "Given config path does not exist"),
- NoConfigPath => write!(f, "The --config switch requires a path value"),
- InvalidArguments => write!(f, "The arguments given are invalid try --help"),
- NoProgramName => write!(f, "Could not get the program name from the environment"),
- NoConfigDir => write!(f, "Could not get the config dir"),
- }
- }
-}
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
deleted file mode 100644
index 8c8e88e..0000000
--- a/src/keyboard/handlers.rs
+++ /dev/null
@@ -1,370 +0,0 @@
-use std::io::Result;
-
-use serde::Deserialize;
-use x11rb::connection::Connection;
-use x11rb::protocol::xproto::*;
-
-use crate::errors::X11Error;
-use crate::keyboard::keysyms::{self, Keysym, format_keysym};
-
-/// When adding a new action, update:
-/// 1. Add variant here
-/// 2. lua_api.rs: string_to_action()
-/// 3. lua_api.rs: register_*_module()
-/// 4. window_manager.rs: handle_key_action()
-/// 5. (optionally) overlay/keybind.rs: action_description()
-/// 6. templates/oxwm.lua
-#[derive(Debug, Copy, Clone, Deserialize, PartialEq)]
-pub enum KeyAction {
- Spawn,
- SpawnTerminal,
- KillClient,
- FocusStack,
- MoveStack,
- Quit,
- Restart,
- ViewTag,
- ViewNextTag,
- ViewPreviousTag,
- ViewNextNonEmptyTag,
- ViewPreviousNonEmptyTag,
- ToggleView,
- MoveToTag,
- ToggleTag,
- ToggleGaps,
- ToggleFullScreen,
- ToggleFloating,
- ChangeLayout,
- CycleLayout,
- FocusMonitor,
- TagMonitor,
- ShowKeybindOverlay,
- SetMasterFactor,
- IncNumMaster,
- ScrollLeft,
- ScrollRight,
- None,
-}
-
-#[derive(Debug, Clone)]
-pub enum Arg {
- None,
- Int(i32),
- Str(String),
- Array(Vec<String>),
-}
-
-impl Arg {
- pub const fn none() -> Self {
- Arg::None
- }
-}
-
-#[derive(Clone)]
-pub struct KeyPress {
- pub(crate) modifiers: Vec<KeyButMask>,
- pub(crate) keysym: Keysym,
-}
-
-impl std::fmt::Debug for KeyPress {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("KeyPress")
- .field("modifiers", &self.modifiers)
- .field("keysym", &format_keysym(self.keysym))
- .finish()
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct KeyBinding {
- pub(crate) keys: Vec<KeyPress>,
- pub(crate) func: KeyAction,
- pub(crate) arg: Arg,
-}
-
-impl KeyBinding {
- pub fn new(keys: Vec<KeyPress>, func: KeyAction, arg: Arg) -> Self {
- Self { keys, func, arg }
- }
-
- pub fn single_key(
- modifiers: Vec<KeyButMask>,
- keysym: Keysym,
- func: KeyAction,
- arg: Arg,
- ) -> Self {
- Self {
- keys: vec![KeyPress { modifiers, keysym }],
- func,
- arg,
- }
- }
-}
-
-pub type Key = KeyBinding;
-
-#[derive(Debug, Clone)]
-pub enum KeychordState {
- Idle,
- InProgress {
- candidates: Vec<usize>,
- keys_pressed: usize,
- },
-}
-
-pub enum KeychordResult {
- Completed(KeyAction, Arg),
- InProgress(Vec<usize>),
- None,
- Cancelled,
-}
-
-pub fn modifiers_to_mask(modifiers: &[KeyButMask]) -> u16 {
- modifiers
- .iter()
- .fold(0u16, |acc, &modifier| acc | u16::from(modifier))
-}
-
-pub struct KeyboardMapping {
- pub syms: Vec<Keysym>,
- pub keysyms_per_keycode: u8,
- pub min_keycode: Keycode,
-}
-
-impl KeyboardMapping {
- pub fn keycode_to_keysym(&self, keycode: Keycode) -> Keysym {
- if keycode < self.min_keycode {
- return 0;
- }
- let index = (keycode - self.min_keycode) as usize * self.keysyms_per_keycode as usize;
- self.syms.get(index).copied().unwrap_or(0)
- }
-
- pub fn find_keycode(
- &self,
- keysym: Keysym,
- min_keycode: Keycode,
- max_keycode: Keycode,
- ) -> Option<Keycode> {
- for keycode in min_keycode..=max_keycode {
- let index = (keycode - self.min_keycode) as usize * self.keysyms_per_keycode as usize;
- if let Some(&sym) = self.syms.get(index)
- && sym == keysym
- {
- return Some(keycode);
- }
- }
- None
- }
-}
-
-pub fn get_keyboard_mapping(
- connection: &impl Connection,
-) -> std::result::Result<KeyboardMapping, X11Error> {
- let setup = connection.setup();
- let min_keycode = setup.min_keycode;
- let max_keycode = setup.max_keycode;
-
- let mapping = connection
- .get_keyboard_mapping(min_keycode, max_keycode - min_keycode + 1)?
- .reply()?;
-
- Ok(KeyboardMapping {
- syms: mapping.keysyms,
- keysyms_per_keycode: mapping.keysyms_per_keycode,
- min_keycode,
- })
-}
-
-pub fn grab_keys(
- connection: &impl Connection,
- root: Window,
- keybindings: &[KeyBinding],
- current_key: usize,
-) -> std::result::Result<KeyboardMapping, X11Error> {
- let setup = connection.setup();
- let min_keycode = setup.min_keycode;
- let max_keycode = setup.max_keycode;
-
- let mapping = get_keyboard_mapping(connection)?;
-
- connection.ungrab_key(x11rb::protocol::xproto::Grab::ANY, root, ModMask::ANY)?;
-
- let modifiers = [
- 0u16,
- u16::from(ModMask::LOCK),
- u16::from(ModMask::M2),
- u16::from(ModMask::LOCK | ModMask::M2),
- ];
-
- for keycode in min_keycode..=max_keycode {
- for keybinding in keybindings {
- if current_key >= keybinding.keys.len() {
- continue;
- }
-
- let key = &keybinding.keys[current_key];
- if key.keysym == mapping.keycode_to_keysym(keycode) {
- let modifier_mask = modifiers_to_mask(&key.modifiers);
- for &ignore_mask in &modifiers {
- connection.grab_key(
- true,
- root,
- (modifier_mask | ignore_mask).into(),
- keycode,
- GrabMode::ASYNC,
- GrabMode::ASYNC,
- )?;
- }
- }
- }
- }
-
- if current_key > 0
- && let Some(escape_keycode) =
- mapping.find_keycode(keysyms::XK_ESCAPE, min_keycode, max_keycode)
- {
- connection.grab_key(
- true,
- root,
- ModMask::ANY,
- escape_keycode,
- GrabMode::ASYNC,
- GrabMode::ASYNC,
- )?;
- }
-
- connection.flush()?;
- Ok(mapping)
-}
-
-pub fn handle_key_press(
- event: KeyPressEvent,
- keybindings: &[KeyBinding],
- keychord_state: &KeychordState,
- mapping: &KeyboardMapping,
-) -> KeychordResult {
- let keysym = mapping.keycode_to_keysym(event.detail);
-
- if keysym == keysyms::XK_ESCAPE {
- return match keychord_state {
- KeychordState::InProgress { .. } => KeychordResult::Cancelled,
- KeychordState::Idle => KeychordResult::None,
- };
- }
-
- match keychord_state {
- KeychordState::Idle => handle_first_key(event, keysym, keybindings),
- KeychordState::InProgress {
- candidates,
- keys_pressed,
- } => handle_next_key(event, keysym, keybindings, candidates, *keys_pressed),
- }
-}
-
-fn handle_first_key(
- event: KeyPressEvent,
- event_keysym: Keysym,
- keybindings: &[KeyBinding],
-) -> KeychordResult {
- let mut candidates = Vec::new();
-
- let clean_state = event.state & !(u16::from(ModMask::LOCK) | u16::from(ModMask::M2));
-
- for (keybinding_index, keybinding) in keybindings.iter().enumerate() {
- if keybinding.keys.is_empty() {
- continue;
- }
-
- let first_key = &keybinding.keys[0];
- let modifier_mask = modifiers_to_mask(&first_key.modifiers);
-
- if event_keysym == first_key.keysym && clean_state == modifier_mask.into() {
- if keybinding.keys.len() == 1 {
- return KeychordResult::Completed(keybinding.func, keybinding.arg.clone());
- } else {
- candidates.push(keybinding_index);
- }
- }
- }
-
- if candidates.is_empty() {
- KeychordResult::None
- } else {
- KeychordResult::InProgress(candidates)
- }
-}
-
-fn handle_next_key(
- event: KeyPressEvent,
- event_keysym: Keysym,
- keybindings: &[KeyBinding],
- candidates: &[usize],
- keys_pressed: usize,
-) -> KeychordResult {
- let mut new_candidates = Vec::new();
-
- let clean_state = event.state & !(u16::from(ModMask::LOCK) | u16::from(ModMask::M2));
-
- for &candidate_index in candidates {
- let keybinding = &keybindings[candidate_index];
-
- if keys_pressed >= keybinding.keys.len() {
- continue;
- }
-
- let next_key = &keybinding.keys[keys_pressed];
- let required_mask = modifiers_to_mask(&next_key.modifiers);
-
- let modifiers_match = if next_key.modifiers.is_empty() {
- true
- } else {
- (clean_state & required_mask) == required_mask.into()
- };
-
- if event_keysym == next_key.keysym && modifiers_match {
- if keys_pressed + 1 == keybinding.keys.len() {
- return KeychordResult::Completed(keybinding.func, keybinding.arg.clone());
- } else {
- new_candidates.push(candidate_index);
- }
- }
- }
-
- if new_candidates.is_empty() {
- KeychordResult::Cancelled
- } else {
- KeychordResult::InProgress(new_candidates)
- }
-}
-
-pub fn handle_spawn_action(action: KeyAction, arg: &Arg, selected_monitor: usize) -> Result<()> {
- if let KeyAction::Spawn = action {
- match arg {
- Arg::Str(command) => {
- crate::signal::spawn_detached(command);
- }
- Arg::Array(command) => {
- let Some((cmd, args)) = command.split_first() else {
- return Ok(());
- };
-
- let mut args_vec: Vec<String> = args.to_vec();
-
- let is_dmenu = cmd.contains("dmenu");
- let has_monitor_flag = args.iter().any(|arg| arg == "-m");
-
- if is_dmenu && !has_monitor_flag {
- args_vec.insert(0, selected_monitor.to_string());
- args_vec.insert(0, "-m".to_string());
- }
-
- let args_str: Vec<&str> = args_vec.iter().map(|s| s.as_str()).collect();
- crate::signal::spawn_detached_with_args(cmd, &args_str);
- }
- _ => {}
- }
- }
-
- Ok(())
-}
diff --git a/src/keyboard/keysyms.rs b/src/keyboard/keysyms.rs
deleted file mode 100644
index d4c1837..0000000
--- a/src/keyboard/keysyms.rs
+++ /dev/null
@@ -1,422 +0,0 @@
-#![allow(dead_code)]
-
-pub type Keysym = u32;
-pub const XK_ESCAPE: Keysym = 0xff1b;
-pub const XK_RETURN: Keysym = 0xff0d;
-pub const XK_SPACE: Keysym = 0x0020;
-pub const XK_TAB: Keysym = 0xff09;
-pub const XK_BACKSPACE: Keysym = 0xff08;
-pub const XK_DELETE: Keysym = 0xffff;
-pub const XK_F1: Keysym = 0xffbe;
-pub const XK_F2: Keysym = 0xffbf;
-pub const XK_F3: Keysym = 0xffc0;
-pub const XK_F4: Keysym = 0xffc1;
-pub const XK_F5: Keysym = 0xffc2;
-pub const XK_F6: Keysym = 0xffc3;
-pub const XK_F7: Keysym = 0xffc4;
-pub const XK_F8: Keysym = 0xffc5;
-pub const XK_F9: Keysym = 0xffc6;
-pub const XK_F10: Keysym = 0xffc7;
-pub const XK_F11: Keysym = 0xffc8;
-pub const XK_F12: Keysym = 0xffc9;
-pub const XK_A: Keysym = 0x0061;
-pub const XK_B: Keysym = 0x0062;
-pub const XK_C: Keysym = 0x0063;
-pub const XK_D: Keysym = 0x0064;
-pub const XK_E: Keysym = 0x0065;
-pub const XK_F: Keysym = 0x0066;
-pub const XK_G: Keysym = 0x0067;
-pub const XK_H: Keysym = 0x0068;
-pub const XK_I: Keysym = 0x0069;
-pub const XK_J: Keysym = 0x006a;
-pub const XK_K: Keysym = 0x006b;
-pub const XK_L: Keysym = 0x006c;
-pub const XK_M: Keysym = 0x006d;
-pub const XK_N: Keysym = 0x006e;
-pub const XK_O: Keysym = 0x006f;
-pub const XK_P: Keysym = 0x0070;
-pub const XK_Q: Keysym = 0x0071;
-pub const XK_R: Keysym = 0x0072;
-pub const XK_S: Keysym = 0x0073;
-pub const XK_T: Keysym = 0x0074;
-pub const XK_U: Keysym = 0x0075;
-pub const XK_V: Keysym = 0x0076;
-pub const XK_W: Keysym = 0x0077;
-pub const XK_X: Keysym = 0x0078;
-pub const XK_Y: Keysym = 0x0079;
-pub const XK_Z: Keysym = 0x007a;
-pub const XK_0: Keysym = 0x0030;
-pub const XK_1: Keysym = 0x0031;
-pub const XK_2: Keysym = 0x0032;
-pub const XK_3: Keysym = 0x0033;
-pub const XK_4: Keysym = 0x0034;
-pub const XK_5: Keysym = 0x0035;
-pub const XK_6: Keysym = 0x0036;
-pub const XK_7: Keysym = 0x0037;
-pub const XK_8: Keysym = 0x0038;
-pub const XK_9: Keysym = 0x0039;
-pub const XK_LEFT: Keysym = 0xff51;
-pub const XK_UP: Keysym = 0xff52;
-pub const XK_RIGHT: Keysym = 0xff53;
-pub const XK_DOWN: Keysym = 0xff54;
-pub const XK_HOME: Keysym = 0xff50;
-pub const XK_END: Keysym = 0xff57;
-pub const XK_PAGE_UP: Keysym = 0xff55;
-pub const XK_PAGE_DOWN: Keysym = 0xff56;
-pub const XK_INSERT: Keysym = 0xff63;
-pub const XK_MINUS: Keysym = 0x002d;
-pub const XK_EQUAL: Keysym = 0x003d;
-pub const XK_LEFT_BRACKET: Keysym = 0x005b;
-pub const XK_RIGHT_BRACKET: Keysym = 0x005d;
-pub const XK_SEMICOLON: Keysym = 0x003b;
-pub const XK_QUESTION: Keysym = 0x003f;
-pub const XK_APOSTROPHE: Keysym = 0x0027;
-pub const XK_GRAVE: Keysym = 0x0060;
-pub const XK_BACKSLASH: Keysym = 0x005c;
-pub const XK_COMMA: Keysym = 0x002c;
-pub const XK_PERIOD: Keysym = 0x002e;
-pub const XK_SLASH: Keysym = 0x002f;
-pub const XK_PRINT: Keysym = 0xff61;
-pub const XK_AMPERSAND: Keysym = 0x26;
-pub const XK_AGRAVE: Keysym = 0xe0;
-pub const XK_CCEDILLA: Keysym = 0xe7;
-pub const XK_EGRAVE: Keysym = 0xe8;
-pub const XK_EACUTE: Keysym = 0xe9;
-pub const XK_PARENLEFT: Keysym = 0x28;
-pub const XK_PARENRIGHT: Keysym = 0x29;
-pub const XK_QUOTEDBL: Keysym = 0x22;
-pub const XK_UNDERSCORE: Keysym = 0x5f;
-pub const XK_HYPHEN: Keysym = 0xad;
-pub const XF86_AUDIO_MEDIA: Keysym = 0x1008ff32;
-pub const XF86_AUDIO_NEXT: Keysym = 0x1008ff17;
-pub const XF86_AUDIO_PAUSE: Keysym = 0x1008ff31;
-pub const XF86_AUDIO_PLAY: Keysym = 0x1008ff14;
-pub const XF86_AUDIO_PREV: Keysym = 0x1008ff16;
-pub const XF86_AUDIO_RAISE_VOLUME: Keysym = 0x1008ff13;
-pub const XF86_AUDIO_LOWER_VOLUME: Keysym = 0x1008ff11;
-pub const XF86_AUDIO_MUTE: Keysym = 0x1008ff12;
-pub const XF86_AUDIO_MIC_MUTE: Keysym = 0x1008ffb2;
-pub const XF86_AUDIO_RECORD: Keysym = 0x1008ff1c;
-pub const XF86_AUDIO_REWIND: Keysym = 0x1008ff3e;
-pub const XF86_AUDIO_STOP: Keysym = 0x1008ff15;
-pub const XF86_BACK: Keysym = 0x1008ff26;
-pub const XF86_CALCULATOR: Keysym = 0x1008ff1d;
-pub const XF86_CLOSE: Keysym = 0x1008ff56;
-pub const XF86_COPY: Keysym = 0x1008ff57;
-pub const XF86_CUT: Keysym = 0x1008ff58;
-pub const XF86_DOS: Keysym = 0x1008ff5a;
-pub const XF86_DISPLAY: Keysym = 0x1008ff59;
-pub const XF86_DOCUMENTS: Keysym = 0x1008ff5b;
-pub const XF86_EJECT: Keysym = 0x1008ff2c;
-pub const XF86_EXPLORER: Keysym = 0x1008ff5d;
-pub const XF86_FAVORITES: Keysym = 0x1008ff30;
-pub const XF86_FINANCE: Keysym = 0x1008ff3c;
-pub const XF86_FORWARD: Keysym = 0x1008ff27;
-pub const XF86_GAME: Keysym = 0x1008ff5e;
-pub const XF86_GO: Keysym = 0x1008ff5f;
-pub const XF86_HOME_PAGE: Keysym = 0x1008ff18;
-pub const XF86_MAIL: Keysym = 0x1008ff19;
-pub const XF86_MAIL_FORWARD: Keysym = 0x1008ff90;
-pub const XF86_MENU_KB: Keysym = 0x1008ff65;
-pub const XF86_MESSENGER: Keysym = 0x1008ff8e;
-pub const XF86_MON_BRIGHTNESS_UP: Keysym = 0x1008ff02;
-pub const XF86_MON_BRIGHTNESS_DOWN: Keysym = 0x1008ff03;
-pub const XF86_MY_COMPUTER: Keysym = 0x1008ff33;
-pub const XF86_NEW: Keysym = 0x1008ff68;
-pub const XF86_NEXT_VMODE: Keysym = 0x1008fe22;
-pub const XF86_PREV_VMODE: Keysym = 0x1008fe23;
-pub const XF86_OPEN: Keysym = 0x1008ff6b;
-pub const XF86_PASTE: Keysym = 0x1008ff6d;
-pub const XF86_PHONE: Keysym = 0x1008ff6e;
-pub const XF86_POWER_OFF: Keysym = 0x1008ff2a;
-pub const XF86_RELOAD: Keysym = 0x1008ff73;
-pub const XF86_REPLY: Keysym = 0x1008ff72;
-pub const XF86_ROTATE_WINDOWS: Keysym = 0x1008ff74;
-pub const XF86_SAVE: Keysym = 0x1008ff77;
-pub const XF86_SCREEN_SAVER: Keysym = 0x1008ff2d;
-pub const XF86_SCROLL_DOWN: Keysym = 0x1008ff79;
-pub const XF86_SCROLL_UP: Keysym = 0x1008ff78;
-pub const XF86_SEARCH: Keysym = 0x1008ff1b;
-pub const XF86_SEND: Keysym = 0x1008ff7b;
-pub const XF86_SHOP: Keysym = 0x1008ff36;
-pub const XF86_SLEEP: Keysym = 0x1008ff2f;
-pub const XF86_TASK_PANE: Keysym = 0x1008ff7f;
-pub const XF86_TOOLS: Keysym = 0x1008ff81;
-pub const XF86_WWW: Keysym = 0x1008ff2e;
-pub const XF86_WAKE_UP: Keysym = 0x1008ff2b;
-pub const XF86_WEBCAM: Keysym = 0x1008ff8f;
-pub const XF86_XFER: Keysym = 0x1008ff8a;
-
-pub fn keysym_from_str(s: &str) -> Option<Keysym> {
- match s {
- "Return" => Some(XK_RETURN),
- "Escape" => Some(XK_ESCAPE),
- "Space" => Some(XK_SPACE),
- "Tab" => Some(XK_TAB),
- "Backspace" => Some(XK_BACKSPACE),
- "Delete" => Some(XK_DELETE),
- "F1" => Some(XK_F1),
- "F2" => Some(XK_F2),
- "F3" => Some(XK_F3),
- "F4" => Some(XK_F4),
- "F5" => Some(XK_F5),
- "F6" => Some(XK_F6),
- "F7" => Some(XK_F7),
- "F8" => Some(XK_F8),
- "F9" => Some(XK_F9),
- "F10" => Some(XK_F10),
- "F11" => Some(XK_F11),
- "F12" => Some(XK_F12),
- "A" => Some(XK_A),
- "B" => Some(XK_B),
- "C" => Some(XK_C),
- "D" => Some(XK_D),
- "E" => Some(XK_E),
- "F" => Some(XK_F),
- "G" => Some(XK_G),
- "H" => Some(XK_H),
- "I" => Some(XK_I),
- "J" => Some(XK_J),
- "K" => Some(XK_K),
- "L" => Some(XK_L),
- "M" => Some(XK_M),
- "N" => Some(XK_N),
- "O" => Some(XK_O),
- "P" => Some(XK_P),
- "Q" => Some(XK_Q),
- "R" => Some(XK_R),
- "S" => Some(XK_S),
- "T" => Some(XK_T),
- "U" => Some(XK_U),
- "V" => Some(XK_V),
- "W" => Some(XK_W),
- "X" => Some(XK_X),
- "Y" => Some(XK_Y),
- "Z" => Some(XK_Z),
- "0" => Some(XK_0),
- "1" => Some(XK_1),
- "2" => Some(XK_2),
- "3" => Some(XK_3),
- "4" => Some(XK_4),
- "5" => Some(XK_5),
- "6" => Some(XK_6),
- "7" => Some(XK_7),
- "8" => Some(XK_8),
- "9" => Some(XK_9),
- "Left" => Some(XK_LEFT),
- "Right" => Some(XK_RIGHT),
- "Up" => Some(XK_UP),
- "Down" => Some(XK_DOWN),
- "Home" => Some(XK_HOME),
- "End" => Some(XK_END),
- "PageUp" => Some(XK_PAGE_UP),
- "PageDown" => Some(XK_PAGE_DOWN),
- "Insert" => Some(XK_INSERT),
- "Minus" => Some(XK_MINUS),
- "Equal" => Some(XK_EQUAL),
- "BracketLeft" => Some(XK_LEFT_BRACKET),
- "BracketRight" => Some(XK_RIGHT_BRACKET),
- "Semicolon" => Some(XK_SEMICOLON),
- "Question" => Some(XK_QUESTION),
- "Apostrophe" => Some(XK_APOSTROPHE),
- "Grave" => Some(XK_GRAVE),
- "Backslash" => Some(XK_BACKSLASH),
- "Comma" => Some(XK_COMMA),
- "Period" => Some(XK_PERIOD),
- "Slash" => Some(XK_SLASH),
- "Print" => Some(XK_PRINT),
- "Ampersand" => Some(XK_AMPERSAND),
- "Agrave" => Some(XK_AGRAVE),
- "Ccedilla" => Some(XK_CCEDILLA),
- "Egrave" => Some(XK_EGRAVE),
- "Eacute" => Some(XK_EACUTE),
- "ParenLeft" => Some(XK_PARENLEFT),
- "ParenRight" => Some(XK_PARENRIGHT),
- "QuoteDouble" => Some(XK_QUOTEDBL),
- "Underscore" => Some(XK_UNDERSCORE),
- "Hyphen" => Some(XK_HYPHEN),
- "AudioMedia" | "XF86AudioMedia" => Some(XF86_AUDIO_MEDIA),
- "XF86AudioNext" => Some(XF86_AUDIO_NEXT),
- "XF86AudioPause" => Some(XF86_AUDIO_PAUSE),
- "XF86AudioPlay" => Some(XF86_AUDIO_PLAY),
- "XF86AudioPrev" => Some(XF86_AUDIO_PREV),
- "AudioRaiseVolume" | "XF86AudioRaiseVolume" => Some(XF86_AUDIO_RAISE_VOLUME),
- "AudioLowerVolume" | "XF86AudioLowerVolume" => Some(XF86_AUDIO_LOWER_VOLUME),
- "AudioMute" | "XF86AudioMute" => Some(XF86_AUDIO_MUTE),
- "XF86AudioMicMute" => Some(XF86_AUDIO_MIC_MUTE),
- "XF86AudioRecord" => Some(XF86_AUDIO_RECORD),
- "XF86AudioRewind" => Some(XF86_AUDIO_REWIND),
- "XF86AudioStop" => Some(XF86_AUDIO_STOP),
- "XF86Back" => Some(XF86_BACK),
- "XF86Calculator" => Some(XF86_CALCULATOR),
- "XF86Close" => Some(XF86_CLOSE),
- "XF86Copy" => Some(XF86_COPY),
- "XF86Cut" => Some(XF86_CUT),
- "XF86DOS" => Some(XF86_DOS),
- "XF86Display" => Some(XF86_DISPLAY),
- "XF86Documents" => Some(XF86_DOCUMENTS),
- "XF86Eject" => Some(XF86_EJECT),
- "XF86Explorer" => Some(XF86_EXPLORER),
- "XF86Favorites" => Some(XF86_FAVORITES),
- "XF86Finance" => Some(XF86_FINANCE),
- "XF86Forward" => Some(XF86_FORWARD),
- "XF86Game" => Some(XF86_GAME),
- "XF86Go" => Some(XF86_GO),
- "XF86HomePage" => Some(XF86_HOME_PAGE),
- "XF86Mail" => Some(XF86_MAIL),
- "XF86MailForward" => Some(XF86_MAIL_FORWARD),
- "XF86MenuKB" => Some(XF86_MENU_KB),
- "XF86Messenger" => Some(XF86_MESSENGER),
- "MonBrightnessUp" | "XF86MonBrightnessUp" => Some(XF86_MON_BRIGHTNESS_UP),
- "MonBrightnessDown" | "XF86MonBrightnessDown" => Some(XF86_MON_BRIGHTNESS_DOWN),
- "XF86MyComputer" => Some(XF86_MY_COMPUTER),
- "XF86New" => Some(XF86_NEW),
- "XF86Next_VMode" => Some(XF86_NEXT_VMODE),
- "XF86Prev_VMode" => Some(XF86_PREV_VMODE),
- "XF86Open" => Some(XF86_OPEN),
- "XF86Paste" => Some(XF86_PASTE),
- "XF86Phone" => Some(XF86_PHONE),
- "XF86PowerOff" => Some(XF86_POWER_OFF),
- "XF86Reload" => Some(XF86_RELOAD),
- "XF86Reply" => Some(XF86_REPLY),
- "XF86RotateWindows" => Some(XF86_ROTATE_WINDOWS),
- "XF86Save" => Some(XF86_SAVE),
- "XF86ScreenSaver" => Some(XF86_SCREEN_SAVER),
- "XF86ScrollDown" => Some(XF86_SCROLL_DOWN),
- "XF86ScrollUp" => Some(XF86_SCROLL_UP),
- "XF86Search" => Some(XF86_SEARCH),
- "XF86Send" => Some(XF86_SEND),
- "XF86Shop" => Some(XF86_SHOP),
- "XF86Sleep" => Some(XF86_SLEEP),
- "XF86TaskPane" => Some(XF86_TASK_PANE),
- "XF86Tools" => Some(XF86_TOOLS),
- "XF86WWW" => Some(XF86_WWW),
- "XF86WakeUp" => Some(XF86_WAKE_UP),
- "XF86WebCam" => Some(XF86_WEBCAM),
- "XF86Xfer" => Some(XF86_XFER),
- _ => None,
- }
-}
-
-pub fn format_keysym(keysym: Keysym) -> String {
- match keysym {
- XK_RETURN => "Return".to_string(),
- XK_ESCAPE => "Esc".to_string(),
- XK_SPACE => "Space".to_string(),
- XK_TAB => "Tab".to_string(),
- XK_BACKSPACE => "Backspace".to_string(),
- XK_DELETE => "Del".to_string(),
- XK_LEFT => "Left".to_string(),
- XK_RIGHT => "Right".to_string(),
- XK_UP => "Up".to_string(),
- XK_DOWN => "Down".to_string(),
- XK_HOME => "Home".to_string(),
- XK_END => "End".to_string(),
- XK_PAGE_UP => "PgUp".to_string(),
- XK_PAGE_DOWN => "PgDn".to_string(),
- XK_INSERT => "Ins".to_string(),
- XK_F1 => "F1".to_string(),
- XK_F2 => "F2".to_string(),
- XK_F3 => "F3".to_string(),
- XK_F4 => "F4".to_string(),
- XK_F5 => "F5".to_string(),
- XK_F6 => "F6".to_string(),
- XK_F7 => "F7".to_string(),
- XK_F8 => "F8".to_string(),
- XK_F9 => "F9".to_string(),
- XK_F10 => "F10".to_string(),
- XK_F11 => "F11".to_string(),
- XK_F12 => "F12".to_string(),
- XK_SLASH => "/".to_string(),
- XK_COMMA => ",".to_string(),
- XK_PERIOD => ".".to_string(),
- XK_MINUS => "-".to_string(),
- XK_EQUAL => "=".to_string(),
- XK_GRAVE => "`".to_string(),
- XK_LEFT_BRACKET => "[".to_string(),
- XK_RIGHT_BRACKET => "]".to_string(),
- XK_SEMICOLON => ";".to_string(),
- XK_QUESTION => "?".to_string(),
- XK_APOSTROPHE => "'".to_string(),
- XK_BACKSLASH => "\\".to_string(),
- XK_PRINT => "Print".to_string(),
- XK_AMPERSAND => "&".to_string(),
- XK_AGRAVE => "à".to_string(),
- XK_CCEDILLA => "ç".to_string(),
- XK_EGRAVE => "è".to_string(),
- XK_EACUTE => "é".to_string(),
- XK_PARENLEFT => "(".to_string(),
- XK_PARENRIGHT => ")".to_string(),
- XK_QUOTEDBL => "\"".to_string(),
- XK_UNDERSCORE => "_".to_string(),
- XK_HYPHEN => "-".to_string(),
- XF86_AUDIO_MEDIA => "Media".to_string(),
- XF86_AUDIO_NEXT => "Next".to_string(),
- XF86_AUDIO_PAUSE => "Pause".to_string(),
- XF86_AUDIO_PLAY => "Play".to_string(),
- XF86_AUDIO_PREV => "Prev".to_string(),
- XF86_AUDIO_RAISE_VOLUME => "Vol+".to_string(),
- XF86_AUDIO_LOWER_VOLUME => "Vol-".to_string(),
- XF86_AUDIO_MUTE => "Mute".to_string(),
- XF86_AUDIO_MIC_MUTE => "MicMute".to_string(),
- XF86_AUDIO_RECORD => "Record".to_string(),
- XF86_AUDIO_REWIND => "Rewind".to_string(),
- XF86_AUDIO_STOP => "Stop".to_string(),
- XF86_BACK => "Back".to_string(),
- XF86_CALCULATOR => "Calculator".to_string(),
- XF86_CLOSE => "Close".to_string(),
- XF86_COPY => "Copy".to_string(),
- XF86_CUT => "Cut".to_string(),
- XF86_DOS => "DOS".to_string(),
- XF86_DISPLAY => "Display".to_string(),
- XF86_DOCUMENTS => "Documents".to_string(),
- XF86_EJECT => "Eject".to_string(),
- XF86_EXPLORER => "Explorer".to_string(),
- XF86_FAVORITES => "Favorites".to_string(),
- XF86_FINANCE => "Finance".to_string(),
- XF86_FORWARD => "Forward".to_string(),
- XF86_GAME => "Game".to_string(),
- XF86_GO => "Go".to_string(),
- XF86_HOME_PAGE => "HomePage".to_string(),
- XF86_MAIL => "Mail".to_string(),
- XF86_MAIL_FORWARD => "MailForward".to_string(),
- XF86_MENU_KB => "MenuKB".to_string(),
- XF86_MESSENGER => "Messenger".to_string(),
- XF86_MON_BRIGHTNESS_UP => "Bright+".to_string(),
- XF86_MON_BRIGHTNESS_DOWN => "Bright-".to_string(),
- XF86_MY_COMPUTER => "MyComputer".to_string(),
- XF86_NEW => "New".to_string(),
- XF86_NEXT_VMODE => "NextVMode".to_string(),
- XF86_PREV_VMODE => "PrevVMode".to_string(),
- XF86_OPEN => "Open".to_string(),
- XF86_PASTE => "Paste".to_string(),
- XF86_PHONE => "Phone".to_string(),
- XF86_POWER_OFF => "PowerOff".to_string(),
- XF86_RELOAD => "Reload".to_string(),
- XF86_REPLY => "Reply".to_string(),
- XF86_ROTATE_WINDOWS => "RotateWindows".to_string(),
- XF86_SAVE => "Save".to_string(),
- XF86_SCREEN_SAVER => "ScreenSaver".to_string(),
- XF86_SCROLL_DOWN => "ScrollDown".to_string(),
- XF86_SCROLL_UP => "ScrollUp".to_string(),
- XF86_SEARCH => "Search".to_string(),
- XF86_SEND => "Send".to_string(),
- XF86_SHOP => "Shop".to_string(),
- XF86_SLEEP => "Sleep".to_string(),
- XF86_TASK_PANE => "TaskPane".to_string(),
- XF86_TOOLS => "Tools".to_string(),
- XF86_WWW => "WWW".to_string(),
- XF86_WAKE_UP => "WakeUp".to_string(),
- XF86_WEBCAM => "WebCam".to_string(),
- XF86_XFER => "Xfer".to_string(),
- XK_A..=XK_Z => {
- let ch = (keysym - XK_A + b'A' as u32) as u8 as char;
- ch.to_string()
- }
- XK_0..=XK_9 => {
- let ch = (keysym - XK_0 + b'0' as u32) as u8 as char;
- ch.to_string()
- }
- _ => format!("0x{:x}", keysym),
- }
-}
diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs
deleted file mode 100644
index c1d2795..0000000
--- a/src/keyboard/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod handlers;
-pub mod keysyms;
-
-pub use handlers::{Arg, KeyAction, KeyboardMapping, grab_keys, handle_key_press};
-pub use keysyms::*;
diff --git a/src/layout/grid.rs b/src/layout/grid.rs
deleted file mode 100644
index 12aa836..0000000
--- a/src/layout/grid.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use super::{GapConfig, Layout, WindowGeometry};
-use x11rb::protocol::xproto::Window;
-
-pub struct GridLayout;
-
-impl Layout for GridLayout {
- fn name(&self) -> &'static str {
- super::LayoutType::Grid.as_str()
- }
-
- fn symbol(&self) -> &'static str {
- "[#]"
- }
-
- fn arrange(
- &self,
- windows: &[Window],
- screen_width: u32,
- screen_height: u32,
- gaps: &GapConfig,
- _master_factor: f32,
- _num_master: i32,
- _smartgaps_enabled: bool,
- ) -> Vec<WindowGeometry> {
- let window_count = windows.len();
- if window_count == 0 {
- return Vec::new();
- }
-
- if window_count == 1 {
- let x = gaps.outer_horizontal as i32;
- let y = gaps.outer_vertical as i32;
- let width = screen_width.saturating_sub(2 * gaps.outer_horizontal);
- let height = screen_height.saturating_sub(2 * gaps.outer_vertical);
-
- return vec![WindowGeometry {
- x_coordinate: x,
- y_coordinate: y,
- width,
- height,
- }];
- }
-
- let cols = (window_count as f64).sqrt().ceil() as usize;
- let rows = (window_count as f64 / cols as f64).ceil() as usize;
-
- let mut geometries = Vec::new();
-
- let total_horizontal_gaps =
- gaps.outer_horizontal * 2 + gaps.inner_horizontal * (cols as u32 - 1);
- let total_vertical_gaps = gaps.outer_vertical * 2 + gaps.inner_vertical * (rows as u32 - 1);
-
- let cell_width = screen_width.saturating_sub(total_horizontal_gaps) / cols as u32;
- let cell_height = screen_height.saturating_sub(total_vertical_gaps) / rows as u32;
-
- for (index, _window) in windows.iter().enumerate() {
- let row = index / cols;
- let col = index % cols;
-
- let is_last_row = row == rows - 1;
- let windows_in_last_row = window_count - (rows - 1) * cols;
-
- let (x, y, width, height) = if is_last_row && windows_in_last_row < cols {
- let last_row_col = index % cols;
- let last_row_cell_width =
- screen_width.saturating_sub(total_horizontal_gaps.saturating_sub(
- gaps.inner_horizontal * (cols as u32 - windows_in_last_row as u32),
- )) / windows_in_last_row as u32;
-
- let x = gaps.outer_horizontal
- + last_row_col as u32 * (last_row_cell_width + gaps.inner_horizontal);
- let y = gaps.outer_vertical + row as u32 * (cell_height + gaps.inner_vertical);
-
- (x as i32, y as i32, last_row_cell_width, cell_height)
- } else {
- let x = gaps.outer_horizontal + col as u32 * (cell_width + gaps.inner_horizontal);
- let y = gaps.outer_vertical + row as u32 * (cell_height + gaps.inner_vertical);
-
- (x as i32, y as i32, cell_width, cell_height)
- };
-
- geometries.push(WindowGeometry {
- x_coordinate: x,
- y_coordinate: y,
- width,
- height,
- });
- }
-
- geometries
- }
-}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
deleted file mode 100644
index e332587..0000000
--- a/src/layout/mod.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-pub mod grid;
-pub mod monocle;
-pub mod normie;
-pub mod scrolling;
-pub mod tabbed;
-pub mod tiling;
-
-use std::str::FromStr;
-
-use x11rb::protocol::xproto::Window;
-
-pub type LayoutBox = Box<dyn Layout>;
-
-pub struct GapConfig {
- pub inner_horizontal: u32,
- pub inner_vertical: u32,
- pub outer_horizontal: u32,
- pub outer_vertical: u32,
-}
-
-pub enum LayoutType {
- Tiling,
- Normie,
- Grid,
- Monocle,
- Tabbed,
- Scrolling,
-}
-
-impl LayoutType {
- pub fn to_boxed_layout(&self) -> LayoutBox {
- match self {
- Self::Tiling => Box::new(tiling::TilingLayout),
- Self::Normie => Box::new(normie::NormieLayout),
- Self::Grid => Box::new(grid::GridLayout),
- Self::Monocle => Box::new(monocle::MonocleLayout),
- Self::Tabbed => Box::new(tabbed::TabbedLayout),
- Self::Scrolling => Box::new(scrolling::ScrollingLayout),
- }
- }
-
- pub fn next(&self) -> Self {
- match self {
- Self::Tiling => Self::Normie,
- Self::Normie => Self::Grid,
- Self::Grid => Self::Monocle,
- Self::Monocle => Self::Tabbed,
- Self::Tabbed => Self::Scrolling,
- Self::Scrolling => Self::Tiling,
- }
- }
-
- pub fn as_str(&self) -> &'static str {
- match self {
- Self::Tiling => "tiling",
- Self::Normie => "normie",
- Self::Grid => "grid",
- Self::Monocle => "monocle",
- Self::Tabbed => "tabbed",
- Self::Scrolling => "scrolling",
- }
- }
-}
-
-impl FromStr for LayoutType {
- type Err = String;
-
- fn from_str(s: &str) -> Result<Self, String> {
- match s.to_lowercase().as_str() {
- "tiling" => Ok(Self::Tiling),
- "normie" | "floating" => Ok(Self::Normie),
- "grid" => Ok(Self::Grid),
- "monocle" => Ok(Self::Monocle),
- "tabbed" => Ok(Self::Tabbed),
- "scrolling" => Ok(Self::Scrolling),
- _ => Err(format!("Invalid Layout Type: {}", s)),
- }
- }
-}
-
-pub fn layout_from_str(s: &str) -> Result<LayoutBox, String> {
- let layout_type = LayoutType::from_str(s)?;
- Ok(layout_type.to_boxed_layout())
-}
-
-pub fn next_layout(current_name: &str) -> &'static str {
- LayoutType::from_str(current_name)
- .ok()
- .map(|layout_type| layout_type.next())
- .unwrap_or(LayoutType::Tiling)
- .as_str()
-}
-
-pub trait Layout {
- fn arrange(
- &self,
- windows: &[Window],
- screen_width: u32,
- screen_height: u32,
- gaps: &GapConfig,
- master_factor: f32,
- num_master: i32,
- smartgaps_enabled: bool,
- ) -> Vec<WindowGeometry>;
- fn name(&self) -> &'static str;
- fn symbol(&self) -> &'static str;
-}
-
-#[derive(Clone)]
-pub struct WindowGeometry {
- pub x_coordinate: i32,
- pub y_coordinate: i32,
- pub width: u32,
- pub height: u32,
-}
diff --git a/src/layout/monocle.rs b/src/layout/monocle.rs
deleted file mode 100644
index 5f511d9..0000000
--- a/src/layout/monocle.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use super::{GapConfig, Layout, WindowGeometry};
-use x11rb::protocol::xproto::Window;
-
-pub struct MonocleLayout;
-
-impl Layout for MonocleLayout {
- fn name(&self) -> &'static str {
- super::LayoutType::Monocle.as_str()
- }
-
- fn symbol(&self) -> &'static str {
- "[M]"
- }
-
- fn arrange(
- &self,
- windows: &[Window],
- screen_width: u32,
- screen_height: u32,
- gaps: &GapConfig,
- _master_factor: f32,
- _num_master: i32,
- _smartgaps_enabled: bool,
- ) -> Vec<WindowGeometry> {
- let window_count = windows.len();
- if window_count == 0 {
- return Vec::new();
- }
-
- let x = gaps.outer_horizontal as i32;
- let y = gaps.outer_vertical as i32;
- let width = screen_width.saturating_sub(2 * gaps.outer_horizontal);
- let height = screen_height.saturating_sub(2 * gaps.outer_vertical);
-
- let geometry = WindowGeometry {
- x_coordinate: x,
- y_coordinate: y,
- width,
- height,
- };
-
- vec![geometry; window_count]
- }
-}
diff --git a/src/layout/normie.rs b/src/layout/normie.rs
deleted file mode 100644
index d299982..0000000
--- a/src/layout/normie.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use super::{GapConfig, Layout, WindowGeometry};
-use x11rb::protocol::xproto::Window;
-
-pub struct NormieLayout;
-
-// This layout should return a no-op similar to DWM.C's "null" mode.
-impl Layout for NormieLayout {
- fn name(&self) -> &'static str {
- super::LayoutType::Normie.as_str()
- }
-
- fn symbol(&self) -> &'static str {
- "><>"
- }
-
- fn arrange(
- &self,
- _windows: &[Window],
- _screen_width: u32,
- _screen_height: u32,
- _gaps: &GapConfig,
- _master_factor: f32,
- _num_master: i32,
- _smartgaps_enabled: bool,
- ) -> Vec<WindowGeometry> {
- Vec::new()
- }
-}
diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs
deleted file mode 100644
index fed8bd6..0000000
--- a/src/layout/scrolling.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use super::{GapConfig, Layout, WindowGeometry};
-use x11rb::protocol::xproto::Window;
-
-pub struct ScrollingLayout;
-
-struct GapValues {
- outer_horizontal: u32,
- outer_vertical: u32,
- inner_vertical: u32,
-}
-
-impl ScrollingLayout {
- fn getgaps(gaps: &GapConfig, window_count: usize, smartgaps_enabled: bool) -> GapValues {
- let outer_enabled = if smartgaps_enabled && window_count == 1 {
- 0
- } else {
- 1
- };
-
- GapValues {
- outer_horizontal: gaps.outer_horizontal * outer_enabled,
- outer_vertical: gaps.outer_vertical * outer_enabled,
- inner_vertical: gaps.inner_vertical,
- }
- }
-}
-
-impl Layout for ScrollingLayout {
- fn name(&self) -> &'static str {
- "scrolling"
- }
-
- fn symbol(&self) -> &'static str {
- "[>>]"
- }
-
- fn arrange(
- &self,
- windows: &[Window],
- screen_width: u32,
- screen_height: u32,
- gaps: &GapConfig,
- _master_factor: f32,
- num_master: i32,
- smartgaps_enabled: bool,
- ) -> Vec<WindowGeometry> {
- let window_count = windows.len();
- if window_count == 0 {
- return Vec::new();
- }
-
- let gap_values = Self::getgaps(gaps, window_count, smartgaps_enabled);
-
- let outer_horizontal = gap_values.outer_horizontal;
- let outer_vertical = gap_values.outer_vertical;
- let inner_vertical = gap_values.inner_vertical;
-
- let visible_count = if num_master > 0 {
- num_master as usize
- } else {
- 2
- };
-
- let available_width = screen_width.saturating_sub(2 * outer_vertical);
- let available_height = screen_height.saturating_sub(2 * outer_horizontal);
-
- let total_inner_gaps = if visible_count > 1 {
- inner_vertical * (visible_count.min(window_count) - 1) as u32
- } else {
- 0
- };
- let window_width = if window_count <= visible_count {
- let num_windows = window_count as u32;
- let total_gaps = if num_windows > 1 {
- inner_vertical * (num_windows - 1)
- } else {
- 0
- };
- (available_width.saturating_sub(total_gaps)) / num_windows
- } else {
- (available_width.saturating_sub(total_inner_gaps)) / visible_count as u32
- };
-
- let mut geometries = Vec::with_capacity(window_count);
- let mut x = outer_vertical as i32;
-
- for _window in windows.iter() {
- geometries.push(WindowGeometry {
- x_coordinate: x,
- y_coordinate: outer_horizontal as i32,
- width: window_width,
- height: available_height,
- });
-
- x += window_width as i32 + inner_vertical as i32;
- }
-
- geometries
- }
-}
diff --git a/src/layout/tabbed.rs b/src/layout/tabbed.rs
deleted file mode 100644
index ab8f26f..0000000
--- a/src/layout/tabbed.rs
+++ /dev/null
@@ -1,48 +0,0 @@
-use super::{GapConfig, Layout, WindowGeometry};
-use x11rb::protocol::xproto::Window;
-
-pub struct TabbedLayout;
-
-pub const TAB_BAR_HEIGHT: u32 = 28;
-
-impl Layout for TabbedLayout {
- fn name(&self) -> &'static str {
- super::LayoutType::Tabbed.as_str()
- }
-
- fn symbol(&self) -> &'static str {
- "[=]"
- }
-
- fn arrange(
- &self,
- windows: &[Window],
- screen_width: u32,
- screen_height: u32,
- gaps: &GapConfig,
- _master_factor: f32,
- _num_master: i32,
- _smartgaps_enabled: bool,
- ) -> Vec<WindowGeometry> {
- let window_count = windows.len();
- if window_count == 0 {
- return Vec::new();
- }
-
- let x = gaps.outer_horizontal as i32;
- let y = (gaps.outer_vertical + TAB_BAR_HEIGHT) as i32;
- let width = screen_width.saturating_sub(2 * gaps.outer_horizontal);
- let height = screen_height
- .saturating_sub(2 * gaps.outer_vertical)
- .saturating_sub(TAB_BAR_HEIGHT);
-
- let geometry = WindowGeometry {
- x_coordinate: x,
- y_coordinate: y,
- width,
- height,
- };
-
- vec![geometry; window_count]
- }
-}
diff --git a/src/layout/tiling.rs b/src/layout/tiling.rs
deleted file mode 100644
index 43bb490..0000000
--- a/src/layout/tiling.rs
+++ /dev/null
@@ -1,173 +0,0 @@
-use super::{GapConfig, Layout, WindowGeometry};
-use x11rb::protocol::xproto::Window;
-
-pub struct TilingLayout;
-
-struct GapValues {
- outer_horizontal: u32,
- outer_vertical: u32,
- inner_horizontal: u32,
- inner_vertical: u32,
-}
-
-struct FactValues {
- master_facts: f32,
- stack_facts: f32,
- master_remainder: i32,
- stack_remainder: i32,
-}
-
-impl TilingLayout {
- fn getgaps(gaps: &GapConfig, window_count: usize, smartgaps_enabled: bool) -> GapValues {
- let outer_enabled = if smartgaps_enabled && window_count == 1 {
- 0
- } else {
- 1
- };
- let inner_enabled = 1;
-
- GapValues {
- outer_horizontal: gaps.outer_horizontal * outer_enabled,
- outer_vertical: gaps.outer_vertical * outer_enabled,
- inner_horizontal: gaps.inner_horizontal * inner_enabled,
- inner_vertical: gaps.inner_vertical * inner_enabled,
- }
- }
-
- fn getfacts(
- window_count: usize,
- num_master: i32,
- master_size: i32,
- stack_size: i32,
- ) -> FactValues {
- let num_master = num_master.max(0) as usize;
- let master_facts = window_count.min(num_master) as f32;
- let stack_facts = if window_count > num_master {
- (window_count - num_master) as f32
- } else {
- 0.0
- };
-
- let mut master_total = 0;
- let mut stack_total = 0;
-
- for i in 0..window_count {
- if i < num_master {
- master_total += (master_size as f32 / master_facts) as i32;
- } else if stack_facts > 0.0 {
- stack_total += (stack_size as f32 / stack_facts) as i32;
- }
- }
-
- FactValues {
- master_facts,
- stack_facts,
- master_remainder: master_size - master_total,
- stack_remainder: stack_size - stack_total,
- }
- }
-}
-
-impl Layout for TilingLayout {
- fn name(&self) -> &'static str {
- super::LayoutType::Tiling.as_str()
- }
-
- fn symbol(&self) -> &'static str {
- "[]="
- }
-
- fn arrange(
- &self,
- windows: &[Window],
- screen_width: u32,
- screen_height: u32,
- gaps: &GapConfig,
- master_factor: f32,
- num_master: i32,
- smartgaps_enabled: bool,
- ) -> Vec<WindowGeometry> {
- let window_count = windows.len();
- if window_count == 0 {
- return Vec::new();
- }
-
- let gap_values = Self::getgaps(gaps, window_count, smartgaps_enabled);
-
- let outer_gap_horizontal = gap_values.outer_horizontal;
- let outer_gap_vertical = gap_values.outer_vertical;
- let inner_gap_horizontal = gap_values.inner_horizontal;
- let inner_gap_vertical = gap_values.inner_vertical;
-
- let mut stack_x = outer_gap_vertical as i32;
- let mut stack_y = outer_gap_horizontal as i32;
- let master_x = outer_gap_vertical as i32;
- let mut master_y = outer_gap_horizontal as i32;
-
- let num_master_usize = num_master.max(0) as usize;
- let master_count = window_count.min(num_master_usize);
- let stack_count = window_count.saturating_sub(num_master_usize);
-
- let master_height = (screen_height as i32)
- - (2 * outer_gap_horizontal) as i32
- - (inner_gap_horizontal as i32 * (master_count.saturating_sub(1)) as i32);
- let stack_height = (screen_height as i32)
- - (2 * outer_gap_horizontal) as i32
- - (inner_gap_horizontal as i32 * stack_count.saturating_sub(1) as i32);
- let mut stack_width = (screen_width as i32) - (2 * outer_gap_vertical) as i32;
- let mut master_width = stack_width;
-
- if num_master > 0 && window_count > num_master_usize {
- stack_width =
- ((master_width as f32 - inner_gap_vertical as f32) * (1.0 - master_factor)) as i32;
- master_width = master_width - inner_gap_vertical as i32 - stack_width;
- stack_x = master_x + master_width + inner_gap_vertical as i32;
- }
-
- let facts = Self::getfacts(window_count, num_master, master_height, stack_height);
-
- let mut geometries = Vec::new();
-
- for (i, _window) in windows.iter().enumerate() {
- if i < num_master_usize {
- let window_height = (master_height as f32 / facts.master_facts) as i32
- + if (i as i32) < facts.master_remainder {
- 1
- } else {
- 0
- };
-
- geometries.push(WindowGeometry {
- x_coordinate: master_x,
- y_coordinate: master_y,
- width: master_width as u32,
- height: window_height as u32,
- });
-
- master_y += window_height + inner_gap_horizontal as i32;
- } else {
- let window_height = if facts.stack_facts > 0.0 {
- (stack_height as f32 / facts.stack_facts) as i32
- + if ((i - num_master_usize) as i32) < facts.stack_remainder {
- 1
- } else {
- 0
- }
- } else {
- stack_height
- };
-
- geometries.push(WindowGeometry {
- x_coordinate: stack_x,
- y_coordinate: stack_y,
- width: stack_width as u32,
- height: window_height as u32,
- });
-
- stack_y += window_height + inner_gap_horizontal as i32;
- }
- }
-
- geometries
- }
-}
diff --git a/src/layouts/floating.zig b/src/layouts/floating.zig
new file mode 100644
index 0000000..27f83fe
--- /dev/null
+++ b/src/layouts/floating.zig
@@ -0,0 +1,6 @@
+const monitor_mod = @import("../monitor.zig");
+
+pub const layout = monitor_mod.Layout{
+ .symbol = "><>",
+ .arrange_fn = null,
+};
diff --git a/src/layouts/monocle.zig b/src/layouts/monocle.zig
new file mode 100644
index 0000000..b6b06f3
--- /dev/null
+++ b/src/layouts/monocle.zig
@@ -0,0 +1,31 @@
+const std = @import("std");
+const client_mod = @import("../client.zig");
+const monitor_mod = @import("../monitor.zig");
+const xlib = @import("../x11/xlib.zig");
+const tiling = @import("tiling.zig");
+
+const Client = client_mod.Client;
+const Monitor = monitor_mod.Monitor;
+
+pub const layout = monitor_mod.Layout{
+ .symbol = "[M]",
+ .arrange_fn = monocle,
+};
+
+pub fn monocle(monitor: *Monitor) void {
+ const gap_h = monitor.gap_outer_h;
+ const gap_v = monitor.gap_outer_v;
+
+ var current = client_mod.next_tiled(monitor.clients);
+ while (current) |client| {
+ tiling.resize(
+ client,
+ monitor.win_x + gap_v,
+ monitor.win_y + gap_h,
+ monitor.win_w - 2 * gap_v - 2 * client.border_width,
+ monitor.win_h - 2 * gap_h - 2 * client.border_width,
+ false,
+ );
+ current = client_mod.next_tiled(client.next);
+ }
+}
diff --git a/src/layouts/scrolling.zig b/src/layouts/scrolling.zig
new file mode 100644
index 0000000..cfb173d
--- /dev/null
+++ b/src/layouts/scrolling.zig
@@ -0,0 +1,114 @@
+const std = @import("std");
+const client_mod = @import("../client.zig");
+const monitor_mod = @import("../monitor.zig");
+const xlib = @import("../x11/xlib.zig");
+const tiling = @import("tiling.zig");
+
+const Client = client_mod.Client;
+const Monitor = monitor_mod.Monitor;
+
+pub const layout = monitor_mod.Layout{
+ .symbol = "[S]",
+ .arrange_fn = scroll,
+};
+
+pub fn scroll(monitor: *Monitor) void {
+ var client_count: u32 = 0;
+ var current = client_mod.next_tiled(monitor.clients);
+ while (current) |_| : (current = client_mod.next_tiled(current.?.next)) {
+ client_count += 1;
+ }
+
+ if (client_count == 0) return;
+
+ const gap_outer_h = monitor.gap_outer_h;
+ const gap_outer_v = monitor.gap_outer_v;
+ const gap_inner_v = monitor.gap_inner_v;
+
+ const visible_count: u32 = @intCast(@max(1, monitor.nmaster));
+ const available_width = monitor.win_w - 2 * gap_outer_v;
+ const available_height = monitor.win_h - 2 * gap_outer_h;
+
+ const total_gaps = gap_inner_v * @as(i32, @intCast(if (visible_count > 1) visible_count - 1 else 0));
+ const window_width = @divTrunc(available_width - total_gaps, @as(i32, @intCast(visible_count)));
+
+ var x_pos: i32 = monitor.win_x + gap_outer_v - monitor.scroll_offset;
+ const y_pos: i32 = monitor.win_y + gap_outer_h;
+ const height = available_height;
+
+ var index: u32 = 0;
+ current = client_mod.next_tiled(monitor.clients);
+ while (current) |client| : (current = client_mod.next_tiled(client.next)) {
+ const window_right = x_pos + window_width;
+ const screen_left = monitor.win_x;
+ const screen_right = monitor.win_x + monitor.win_w;
+ const is_visible = window_right > screen_left and x_pos < screen_right;
+
+ if (is_visible) {
+ tiling.resize(
+ client,
+ x_pos,
+ y_pos,
+ window_width - 2 * client.border_width,
+ height - 2 * client.border_width,
+ false,
+ );
+ } else {
+ tiling.resize_client(
+ client,
+ -2 * window_width,
+ y_pos,
+ window_width - 2 * client.border_width,
+ height - 2 * client.border_width,
+ );
+ }
+ x_pos += window_width + gap_inner_v;
+ index += 1;
+ }
+}
+
+pub fn get_scroll_step(monitor: *Monitor) i32 {
+ const gap_outer_v = monitor.gap_outer_v;
+ const gap_inner_v = monitor.gap_inner_v;
+ const visible_count: u32 = @intCast(@max(1, monitor.nmaster));
+ const available_width = monitor.win_w - 2 * gap_outer_v;
+ const total_gaps = gap_inner_v * @as(i32, @intCast(if (visible_count > 1) visible_count - 1 else 0));
+ const window_width = @divTrunc(available_width - total_gaps, @as(i32, @intCast(visible_count)));
+ return window_width + gap_inner_v;
+}
+
+pub fn get_max_scroll(monitor: *Monitor) i32 {
+ var client_count: u32 = 0;
+ var current = client_mod.next_tiled(monitor.clients);
+ while (current) |_| : (current = client_mod.next_tiled(current.?.next)) {
+ client_count += 1;
+ }
+
+ const visible_count: u32 = @intCast(@max(1, monitor.nmaster));
+ if (client_count <= visible_count) return 0;
+
+ const scroll_step = get_scroll_step(monitor);
+ const scrollable = client_count - visible_count;
+ return scroll_step * @as(i32, @intCast(scrollable));
+}
+
+pub fn get_window_index(monitor: *Monitor, target: *Client) ?u32 {
+ var index: u32 = 0;
+ var current = client_mod.next_tiled(monitor.clients);
+ while (current) |client| : (current = client_mod.next_tiled(client.next)) {
+ if (client == target) return index;
+ index += 1;
+ }
+ return null;
+}
+
+pub fn get_target_scroll_for_window(monitor: *Monitor, target: *Client) i32 {
+ const index = get_window_index(monitor, target) orelse return 0;
+ if (index == 0) return 0;
+
+ const scroll_step = get_scroll_step(monitor);
+ const max_scroll = get_max_scroll(monitor);
+
+ const target_scroll = scroll_step * @as(i32, @intCast(index));
+ return @min(target_scroll, max_scroll);
+}
diff --git a/src/layouts/tiling.zig b/src/layouts/tiling.zig
new file mode 100644
index 0000000..ff8487b
--- /dev/null
+++ b/src/layouts/tiling.zig
@@ -0,0 +1,279 @@
+const std = @import("std");
+const client_mod = @import("../client.zig");
+const monitor_mod = @import("../monitor.zig");
+const xlib = @import("../x11/xlib.zig");
+
+const Client = client_mod.Client;
+const Monitor = monitor_mod.Monitor;
+
+pub const layout = monitor_mod.Layout{
+ .symbol = "[]=",
+ .arrange_fn = tile,
+};
+
+pub var display_handle: ?*xlib.Display = null;
+pub var screen_width: i32 = 0;
+pub var screen_height: i32 = 0;
+pub var bar_height: i32 = 0;
+
+pub fn set_display(display: *xlib.Display) void {
+ display_handle = display;
+}
+
+pub fn set_screen_size(width: i32, height: i32) void {
+ screen_width = width;
+ screen_height = height;
+}
+
+pub fn set_bar_height(height: i32) void {
+ bar_height = height;
+}
+
+pub fn tile(monitor: *Monitor) void {
+ var gap_outer_h: i32 = 0;
+ var gap_outer_v: i32 = 0;
+ var gap_inner_h: i32 = 0;
+ var gap_inner_v: i32 = 0;
+ var client_count: u32 = 0;
+
+ get_gaps(monitor, &gap_outer_h, &gap_outer_v, &gap_inner_h, &gap_inner_v, &client_count);
+ if (client_count == 0) return;
+
+ const nmaster: i32 = monitor.nmaster;
+ const nmaster_count: u32 = @intCast(@max(0, nmaster));
+
+ const master_x: i32 = monitor.win_x + gap_outer_v;
+ var master_y: i32 = monitor.win_y + gap_outer_h;
+ const master_height: i32 = monitor.win_h - 2 * gap_outer_h - gap_inner_h * (@as(i32, @intCast(@min(client_count, nmaster_count))) - 1);
+ var master_width: i32 = monitor.win_w - 2 * gap_outer_v;
+
+ var stack_x: i32 = master_x;
+ var stack_y: i32 = monitor.win_y + gap_outer_h;
+ const stack_height: i32 = monitor.win_h - 2 * gap_outer_h - gap_inner_h * (@as(i32, @intCast(client_count)) - nmaster - 1);
+ var stack_width: i32 = master_width;
+
+ if (nmaster > 0 and client_count > nmaster_count) {
+ stack_width = @intFromFloat(@as(f32, @floatFromInt(master_width - gap_inner_v)) * (1.0 - monitor.mfact));
+ master_width = master_width - gap_inner_v - stack_width;
+ stack_x = master_x + master_width + gap_inner_v;
+ }
+
+ var master_facts: f32 = 0;
+ var stack_facts: f32 = 0;
+ var master_rest: i32 = 0;
+ var stack_rest: i32 = 0;
+ get_facts(monitor, master_height, stack_height, &master_facts, &stack_facts, &master_rest, &stack_rest);
+
+ var index: u32 = 0;
+ var current = client_mod.next_tiled(monitor.clients);
+ while (current) |client| : (current = client_mod.next_tiled(client.next)) {
+ if (index < nmaster_count) {
+ const height = @as(i32, @intFromFloat(@as(f32, @floatFromInt(master_height)) / master_facts)) + (if (index < @as(u32, @intCast(master_rest))) @as(i32, 1) else @as(i32, 0)) - 2 * client.border_width;
+ resize(client, master_x, master_y, master_width - 2 * client.border_width, height, false);
+ master_y += get_client_height(client) + gap_inner_h;
+ } else {
+ const stack_index = index - nmaster_count;
+ const height = @as(i32, @intFromFloat(@as(f32, @floatFromInt(stack_height)) / stack_facts)) + (if (stack_index < @as(u32, @intCast(stack_rest))) @as(i32, 1) else @as(i32, 0)) - 2 * client.border_width;
+ resize(client, stack_x, stack_y, stack_width - 2 * client.border_width, height, false);
+ stack_y += get_client_height(client) + gap_inner_h;
+ }
+ index += 1;
+ }
+}
+
+fn get_gaps(monitor: *Monitor, gap_outer_h: *i32, gap_outer_v: *i32, gap_inner_h: *i32, gap_inner_v: *i32, client_count: *u32) void {
+ var count: u32 = 0;
+ var current = client_mod.next_tiled(monitor.clients);
+ while (current) |client| : (current = client_mod.next_tiled(client.next)) {
+ count += 1;
+ }
+
+ gap_outer_h.* = monitor.gap_outer_h;
+ gap_outer_v.* = monitor.gap_outer_v;
+ gap_inner_h.* = monitor.gap_inner_h;
+ gap_inner_v.* = monitor.gap_inner_v;
+ client_count.* = count;
+}
+
+fn get_facts(monitor: *Monitor, master_size: i32, stack_size: i32, master_factor: *f32, stack_factor: *f32, master_rest: *i32, stack_rest: *i32) void {
+ var count: u32 = 0;
+ var current = client_mod.next_tiled(monitor.clients);
+ while (current) |client| : (current = client_mod.next_tiled(client.next)) {
+ count += 1;
+ }
+
+ const nmaster_count: u32 = @intCast(@max(0, monitor.nmaster));
+ const master_facts: f32 = @floatFromInt(@min(count, nmaster_count));
+ const stack_facts: f32 = @floatFromInt(if (count > nmaster_count) count - nmaster_count else 0);
+
+ var master_total: i32 = 0;
+ var stack_total: i32 = 0;
+
+ if (master_facts > 0) {
+ master_total = @as(i32, @intFromFloat(@as(f32, @floatFromInt(master_size)) / master_facts)) * @as(i32, @intFromFloat(master_facts));
+ }
+ if (stack_facts > 0) {
+ stack_total = @as(i32, @intFromFloat(@as(f32, @floatFromInt(stack_size)) / stack_facts)) * @as(i32, @intFromFloat(stack_facts));
+ }
+
+ master_factor.* = master_facts;
+ stack_factor.* = stack_facts;
+ master_rest.* = master_size - master_total;
+ stack_rest.* = stack_size - stack_total;
+}
+
+fn get_client_width(client: *Client) i32 {
+ return client.width + 2 * client.border_width;
+}
+
+fn get_client_height(client: *Client) i32 {
+ return client.height + 2 * client.border_width;
+}
+
+pub fn apply_size_hints(client: *Client, target_x: *i32, target_y: *i32, target_width: *i32, target_height: *i32, interact: bool) bool {
+ const monitor = client.monitor orelse return false;
+
+ target_width.* = @max(1, target_width.*);
+ target_height.* = @max(1, target_height.*);
+
+ if (interact) {
+ if (target_x.* > screen_width) {
+ target_x.* = screen_width - get_client_width(client);
+ }
+ if (target_y.* > screen_height) {
+ target_y.* = screen_height - get_client_height(client);
+ }
+ if (target_x.* + target_width.* + 2 * client.border_width < 0) {
+ target_x.* = 0;
+ }
+ if (target_y.* + target_height.* + 2 * client.border_width < 0) {
+ target_y.* = 0;
+ }
+ } else {
+ if (target_x.* >= monitor.win_x + monitor.win_w) {
+ target_x.* = monitor.win_x + monitor.win_w - get_client_width(client);
+ }
+ if (target_y.* >= monitor.win_y + monitor.win_h) {
+ target_y.* = monitor.win_y + monitor.win_h - get_client_height(client);
+ }
+ if (target_x.* + target_width.* + 2 * client.border_width <= monitor.win_x) {
+ target_x.* = monitor.win_x;
+ }
+ if (target_y.* + target_height.* + 2 * client.border_width <= monitor.win_y) {
+ target_y.* = monitor.win_y;
+ }
+ }
+
+ if (target_height.* < bar_height) {
+ target_height.* = bar_height;
+ }
+ if (target_width.* < bar_height) {
+ target_width.* = bar_height;
+ }
+
+ if (client.is_floating or monitor.lt[monitor.sel_lt] == null) {
+ const base_is_min = client.base_width == client.min_width and client.base_height == client.min_height;
+
+ var adjusted_width = target_width.*;
+ var adjusted_height = target_height.*;
+
+ if (!base_is_min) {
+ adjusted_width -= client.base_width;
+ adjusted_height -= client.base_height;
+ }
+
+ if (client.min_aspect > 0 and client.max_aspect > 0) {
+ const width_float: f32 = @floatFromInt(adjusted_width);
+ const height_float: f32 = @floatFromInt(adjusted_height);
+ if (client.max_aspect < width_float / height_float) {
+ adjusted_width = @intFromFloat(height_float * client.max_aspect + 0.5);
+ } else if (client.min_aspect < height_float / width_float) {
+ adjusted_height = @intFromFloat(width_float * client.min_aspect + 0.5);
+ }
+ }
+
+ if (base_is_min) {
+ adjusted_width -= client.base_width;
+ adjusted_height -= client.base_height;
+ }
+
+ if (client.increment_width > 0) {
+ adjusted_width -= @mod(adjusted_width, client.increment_width);
+ }
+ if (client.increment_height > 0) {
+ adjusted_height -= @mod(adjusted_height, client.increment_height);
+ }
+
+ target_width.* = @max(adjusted_width + client.base_width, client.min_width);
+ target_height.* = @max(adjusted_height + client.base_height, client.min_height);
+
+ if (client.max_width > 0) {
+ target_width.* = @min(target_width.*, client.max_width);
+ }
+ if (client.max_height > 0) {
+ target_height.* = @min(target_height.*, client.max_height);
+ }
+ }
+
+ return target_x.* != client.x or target_y.* != client.y or target_width.* != client.width or target_height.* != client.height;
+}
+
+pub fn resize(client: *Client, target_x: i32, target_y: i32, target_width: i32, target_height: i32, interact: bool) void {
+ var final_x = target_x;
+ var final_y = target_y;
+ var final_width = target_width;
+ var final_height = target_height;
+
+ if (apply_size_hints(client, &final_x, &final_y, &final_width, &final_height, interact)) {
+ resize_client(client, final_x, final_y, final_width, final_height);
+ }
+}
+
+pub fn resize_client(client: *Client, target_x: i32, target_y: i32, target_width: i32, target_height: i32) void {
+ client.old_x = client.x;
+ client.old_y = client.y;
+ client.old_width = client.width;
+ client.old_height = client.height;
+ client.x = target_x;
+ client.y = target_y;
+ client.width = target_width;
+ client.height = target_height;
+
+ const display = display_handle orelse return;
+
+ var window_changes: xlib.c.XWindowChanges = undefined;
+ window_changes.x = target_x;
+ window_changes.y = target_y;
+ window_changes.width = @intCast(@max(1, target_width));
+ window_changes.height = @intCast(@max(1, target_height));
+ window_changes.border_width = client.border_width;
+
+ _ = xlib.c.XConfigureWindow(
+ display,
+ client.window,
+ xlib.c.CWX | xlib.c.CWY | xlib.c.CWWidth | xlib.c.CWHeight | xlib.c.CWBorderWidth,
+ &window_changes,
+ );
+
+ send_configure(client);
+ _ = xlib.XSync(display, xlib.False);
+}
+
+pub fn send_configure(client: *Client) void {
+ const display = display_handle orelse return;
+
+ var configure_event: xlib.c.XConfigureEvent = undefined;
+ configure_event.type = xlib.c.ConfigureNotify;
+ configure_event.display = display;
+ configure_event.event = client.window;
+ configure_event.window = client.window;
+ configure_event.x = client.x;
+ configure_event.y = client.y;
+ configure_event.width = client.width;
+ configure_event.height = client.height;
+ configure_event.border_width = client.border_width;
+ configure_event.above = xlib.None;
+ configure_event.override_redirect = xlib.False;
+
+ _ = xlib.c.XSendEvent(display, client.window, xlib.False, xlib.StructureNotifyMask, @ptrCast(&configure_event));
+}
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index 378f303..0000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,362 +0,0 @@
-use std::path::PathBuf;
-
-pub mod animations;
-pub mod bar;
-pub mod client;
-pub mod config;
-pub mod errors;
-pub mod keyboard;
-pub mod layout;
-pub mod monitor;
-pub mod overlay;
-pub mod signal;
-pub mod size_hints;
-pub mod tab_bar;
-pub mod window_manager;
-
-pub mod prelude {
- pub use crate::ColorScheme;
- pub use crate::LayoutSymbolOverride;
- pub use crate::WindowRule;
- pub use crate::bar::{BlockCommand, BlockConfig};
- pub use crate::keyboard::{Arg, KeyAction, handlers::KeyBinding, keysyms};
- pub use x11rb::protocol::xproto::KeyButMask;
-}
-
-#[derive(Debug, Clone)]
-pub struct LayoutSymbolOverride {
- pub name: String,
- pub symbol: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct WindowRule {
- pub class: Option<String>,
- pub instance: Option<String>,
- pub title: Option<String>,
- pub tags: Option<u32>,
- pub focus: Option<bool>,
- pub is_floating: Option<bool>,
- pub monitor: Option<usize>,
-}
-
-impl WindowRule {
- pub fn matches(&self, class: &str, instance: &str, title: &str) -> bool {
- let class_matches = self
- .class
- .as_ref()
- .is_none_or(|c| class.contains(c.as_str()));
- let instance_matches = self
- .instance
- .as_ref()
- .is_none_or(|i| instance.contains(i.as_str()));
- let title_matches = self
- .title
- .as_ref()
- .is_none_or(|t| title.contains(t.as_str()));
- class_matches && instance_matches && title_matches
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Config {
- // Meta
- pub path: Option<PathBuf>,
-
- // Appearance
- pub border_width: u32,
- pub border_focused: u32,
- pub border_unfocused: u32,
- pub font: String,
-
- // Gaps
- pub gaps_enabled: bool,
- pub smartgaps_enabled: bool,
- pub gap_inner_horizontal: u32,
- pub gap_inner_vertical: u32,
- pub gap_outer_horizontal: u32,
- pub gap_outer_vertical: u32,
-
- // Basics
- pub terminal: String,
- pub modkey: x11rb::protocol::xproto::KeyButMask,
-
- // Tags
- pub tags: Vec<String>,
-
- // Layout symbol overrides
- pub layout_symbols: Vec<LayoutSymbolOverride>,
-
- // Keybindings
- pub keybindings: Vec<crate::keyboard::handlers::Key>,
- pub tag_back_and_forth: bool,
-
- // Window rules
- pub window_rules: Vec<WindowRule>,
-
- // Status bar
- pub status_blocks: Vec<crate::bar::BlockConfig>,
-
- // Bar color schemes
- pub scheme_normal: ColorScheme,
- pub scheme_occupied: ColorScheme,
- pub scheme_selected: ColorScheme,
- pub scheme_urgent: ColorScheme,
-
- pub autostart: Vec<String>,
- pub auto_tile: bool,
- pub hide_vacant_tags: bool,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct ColorScheme {
- pub foreground: u32,
- pub background: u32,
- pub underline: u32,
-}
-
-impl Default for Config {
- fn default() -> Self {
- use crate::keyboard::handlers::KeyBinding;
- use crate::keyboard::{Arg, KeyAction, keysyms};
- use x11rb::protocol::xproto::KeyButMask;
-
- const MODKEY: KeyButMask = KeyButMask::MOD4;
- const SHIFT: KeyButMask = KeyButMask::SHIFT;
-
- const TERMINAL: &str = "st";
-
- Self {
- path: None,
- border_width: 2,
- border_focused: 0x6dade3,
- border_unfocused: 0xbbbbbb,
- font: "monospace:size=10".to_string(),
- gaps_enabled: false,
- smartgaps_enabled: true,
- gap_inner_horizontal: 0,
- gap_inner_vertical: 0,
- gap_outer_horizontal: 0,
- gap_outer_vertical: 0,
- terminal: TERMINAL.to_string(),
- modkey: MODKEY,
- tags: vec!["1", "2", "3", "4", "5", "6", "7", "8", "9"]
- .into_iter()
- .map(String::from)
- .collect(),
- layout_symbols: vec![],
- keybindings: vec![
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_RETURN,
- KeyAction::Spawn,
- Arg::Str(TERMINAL.to_string()),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_D,
- KeyAction::Spawn,
- Arg::Array(vec![
- "sh".to_string(),
- "-c".to_string(),
- "dmenu_run -l 10".to_string(),
- ]),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_Q,
- KeyAction::KillClient,
- Arg::None,
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_N,
- KeyAction::CycleLayout,
- Arg::None,
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_F,
- KeyAction::ToggleFullScreen,
- Arg::None,
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_A,
- KeyAction::ToggleGaps,
- Arg::None,
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_Q,
- KeyAction::Quit,
- Arg::None,
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_R,
- KeyAction::Restart,
- Arg::None,
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_F,
- KeyAction::ToggleFloating,
- Arg::None,
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_J,
- KeyAction::FocusStack,
- Arg::Int(-1),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_K,
- KeyAction::FocusStack,
- Arg::Int(1),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_1,
- KeyAction::ViewTag,
- Arg::Int(0),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_2,
- KeyAction::ViewTag,
- Arg::Int(1),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_3,
- KeyAction::ViewTag,
- Arg::Int(2),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_4,
- KeyAction::ViewTag,
- Arg::Int(3),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_5,
- KeyAction::ViewTag,
- Arg::Int(4),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_6,
- KeyAction::ViewTag,
- Arg::Int(5),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_7,
- KeyAction::ViewTag,
- Arg::Int(6),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_8,
- KeyAction::ViewTag,
- Arg::Int(7),
- ),
- KeyBinding::single_key(
- vec![MODKEY],
- keysyms::XK_9,
- KeyAction::ViewTag,
- Arg::Int(8),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_1,
- KeyAction::MoveToTag,
- Arg::Int(0),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_2,
- KeyAction::MoveToTag,
- Arg::Int(1),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_3,
- KeyAction::MoveToTag,
- Arg::Int(2),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_4,
- KeyAction::MoveToTag,
- Arg::Int(3),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_5,
- KeyAction::MoveToTag,
- Arg::Int(4),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_6,
- KeyAction::MoveToTag,
- Arg::Int(5),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_7,
- KeyAction::MoveToTag,
- Arg::Int(6),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_8,
- KeyAction::MoveToTag,
- Arg::Int(7),
- ),
- KeyBinding::single_key(
- vec![MODKEY, SHIFT],
- keysyms::XK_9,
- KeyAction::MoveToTag,
- Arg::Int(8),
- ),
- ],
- tag_back_and_forth: false,
- window_rules: vec![],
- status_blocks: vec![crate::bar::BlockConfig {
- format: "{}".to_string(),
- command: crate::bar::BlockCommand::DateTime("%a, %b %d - %-I:%M %P".to_string()),
- interval_secs: 1,
- color: 0x0db9d7,
- underline: true,
- }],
- scheme_normal: ColorScheme {
- foreground: 0xbbbbbb,
- background: 0x1a1b26,
- underline: 0x444444,
- },
- scheme_occupied: ColorScheme {
- foreground: 0x0db9d7,
- background: 0x1a1b26,
- underline: 0x0db9d7,
- },
- scheme_selected: ColorScheme {
- foreground: 0x0db9d7,
- background: 0x1a1b26,
- underline: 0xad8ee6,
- },
- scheme_urgent: ColorScheme {
- foreground: 0xff5555,
- background: 0x1a1b26,
- underline: 0xff5555,
- },
- autostart: vec![],
- auto_tile: false,
- hide_vacant_tags: false,
- }
- }
-}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..df7e022
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,2291 @@
+const std = @import("std");
+const display_mod = @import("x11/display.zig");
+const events = @import("x11/events.zig");
+const xlib = @import("x11/xlib.zig");
+const client_mod = @import("client.zig");
+const monitor_mod = @import("monitor.zig");
+const tiling = @import("layouts/tiling.zig");
+const monocle = @import("layouts/monocle.zig");
+const floating = @import("layouts/floating.zig");
+const scrolling = @import("layouts/scrolling.zig");
+const animations = @import("animations.zig");
+const bar_mod = @import("bar/bar.zig");
+const blocks_mod = @import("bar/blocks/blocks.zig");
+const config_mod = @import("config/config.zig");
+const lua = @import("config/lua.zig");
+
+const Display = display_mod.Display;
+const Client = client_mod.Client;
+const Monitor = monitor_mod.Monitor;
+
+var running: bool = true;
+var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+
+var wm_protocols: xlib.Atom = 0;
+var wm_delete: xlib.Atom = 0;
+var wm_state: xlib.Atom = 0;
+var wm_take_focus: xlib.Atom = 0;
+
+var net_supported: xlib.Atom = 0;
+var net_wm_name: xlib.Atom = 0;
+var net_wm_state: xlib.Atom = 0;
+var net_wm_check: xlib.Atom = 0;
+var net_wm_state_fullscreen: xlib.Atom = 0;
+var net_active_window: xlib.Atom = 0;
+var net_wm_window_type: xlib.Atom = 0;
+var net_wm_window_type_dialog: xlib.Atom = 0;
+var net_client_list: xlib.Atom = 0;
+
+var wm_check_window: xlib.Window = 0;
+
+var border_color_focused: c_ulong = 0x6dade3;
+var border_color_unfocused: c_ulong = 0x444444;
+var border_width: i32 = 2;
+var gap_outer_v: i32 = 5;
+var gap_outer_h: i32 = 5;
+var gap_inner_h: i32 = 5;
+var gap_inner_v: i32 = 5;
+
+var tags: [9][]const u8 = .{ "1", "2", "3", "4", "5", "6", "7", "8", "9" };
+
+var cursor_normal: xlib.Cursor = 0;
+var cursor_resize: xlib.Cursor = 0;
+var cursor_move: xlib.Cursor = 0;
+
+const NormalState: c_long = 1;
+const WithdrawnState: c_long = 0;
+const IconicState: c_long = 3;
+const IsViewable: c_int = 2;
+const snap_distance: i32 = 32;
+
+var numlock_mask: c_uint = 0;
+
+var config: config_mod.Config = undefined;
+var display_global: ?*Display = null;
+var config_path_global: ?[]const u8 = null;
+
+var scroll_animation: animations.Scroll_Animation = .{};
+var animation_config: animations.Animation_Config = .{ .duration_ms = 150, .easing = .ease_out };
+
+pub fn main() !void {
+ const allocator = gpa.allocator();
+ defer _ = gpa.deinit();
+
+ std.debug.print("oxwm starting\n", .{});
+
+ var config_path: ?[]const u8 = null;
+ var args = std.process.args();
+ _ = args.skip();
+ while (args.next()) |arg| {
+ if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--config")) {
+ config_path = args.next();
+ } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
+ std.debug.print("usage: oxwm [-c config.lua]\n", .{});
+ return;
+ }
+ }
+
+ config = config_mod.Config.init(allocator);
+ defer config.deinit();
+ config_mod.set_config(&config);
+
+ if (lua.init(&config)) {
+ const loaded = if (config_path) |path|
+ lua.load_file(path)
+ else
+ lua.load_config();
+
+ if (loaded) {
+ config_path_global = config_path;
+ if (config_path) |path| {
+ std.debug.print("loaded config from {s}\n", .{path});
+ } else {
+ std.debug.print("loaded config from ~/.config/oxwm/config.lua\n", .{});
+ }
+ apply_config_values();
+ } else {
+ std.debug.print("no config found, using defaults\n", .{});
+ setup_default_keybinds();
+ }
+ } else {
+ std.debug.print("failed to init lua, using defaults\n", .{});
+ setup_default_keybinds();
+ }
+
+ var display = Display.open() catch |err| {
+ std.debug.print("failed to open display: {}\n", .{err});
+ return;
+ };
+ defer display.close();
+
+ display_global = &display;
+
+ std.debug.print("display opened: screen={d} root=0x{x}\n", .{ display.screen, display.root });
+ std.debug.print("screen size: {d}x{d}\n", .{ display.screen_width(), display.screen_height() });
+
+ display.become_window_manager() catch |err| {
+ std.debug.print("failed to become window manager: {}\n", .{err});
+ return;
+ };
+
+ std.debug.print("successfully became window manager\n", .{});
+
+ setup_atoms(&display);
+ setup_cursors(&display);
+ client_mod.init(allocator);
+ monitor_mod.init(allocator);
+ monitor_mod.set_root_window(display.root, display.handle);
+ tiling.set_display(display.handle);
+ tiling.set_screen_size(display.screen_width(), display.screen_height());
+
+ setup_monitors(&display);
+ setup_bars(allocator, &display);
+ grab_keybinds(&display);
+ scan_existing_windows(&display);
+
+ std.debug.print("entering event loop\n", .{});
+ run_event_loop(&display);
+
+ lua.deinit();
+ std.debug.print("oxwm exiting\n", .{});
+}
+
+fn setup_atoms(display: *Display) void {
+ wm_protocols = xlib.XInternAtom(display.handle, "WM_PROTOCOLS", xlib.False);
+ wm_delete = xlib.XInternAtom(display.handle, "WM_DELETE_WINDOW", xlib.False);
+ wm_state = xlib.XInternAtom(display.handle, "WM_STATE", xlib.False);
+ wm_take_focus = xlib.XInternAtom(display.handle, "WM_TAKE_FOCUS", xlib.False);
+
+ net_active_window = xlib.XInternAtom(display.handle, "_NET_ACTIVE_WINDOW", xlib.False);
+ net_supported = xlib.XInternAtom(display.handle, "_NET_SUPPORTED", xlib.False);
+ net_wm_name = xlib.XInternAtom(display.handle, "_NET_WM_NAME", xlib.False);
+ net_wm_state = xlib.XInternAtom(display.handle, "_NET_WM_STATE", xlib.False);
+ net_wm_check = xlib.XInternAtom(display.handle, "_NET_SUPPORTING_WM_CHECK", xlib.False);
+ net_wm_state_fullscreen = xlib.XInternAtom(display.handle, "_NET_WM_STATE_FULLSCREEN", xlib.False);
+ net_wm_window_type = xlib.XInternAtom(display.handle, "_NET_WM_WINDOW_TYPE", xlib.False);
+ net_wm_window_type_dialog = xlib.XInternAtom(display.handle, "_NET_WM_WINDOW_TYPE_DIALOG", xlib.False);
+ net_client_list = xlib.XInternAtom(display.handle, "_NET_CLIENT_LIST", xlib.False);
+
+ const utf8_string = xlib.XInternAtom(display.handle, "UTF8_STRING", xlib.False);
+
+ wm_check_window = xlib.XCreateSimpleWindow(display.handle, display.root, 0, 0, 1, 1, 0, 0, 0);
+ _ = xlib.XChangeProperty(display.handle, wm_check_window, net_wm_check, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&wm_check_window), 1);
+ _ = xlib.XChangeProperty(display.handle, wm_check_window, net_wm_name, utf8_string, 8, xlib.PropModeReplace, "oxwm", 6);
+ _ = xlib.XChangeProperty(display.handle, display.root, net_wm_check, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&wm_check_window), 1);
+
+ var net_atoms = [_]xlib.Atom{ net_supported, net_wm_name, net_wm_state, net_wm_check, net_wm_state_fullscreen, net_active_window, net_wm_window_type, net_wm_window_type_dialog, net_client_list };
+ _ = xlib.XChangeProperty(display.handle, display.root, net_supported, xlib.XA_ATOM, 32, xlib.PropModeReplace, @ptrCast(&net_atoms), net_atoms.len);
+
+ _ = xlib.XDeleteProperty(display.handle, display.root, net_client_list);
+
+ std.debug.print("atoms initialized with EWMH support\n", .{});
+}
+
+fn setup_cursors(display: *Display) void {
+ cursor_normal = xlib.XCreateFontCursor(display.handle, xlib.XC_left_ptr);
+ cursor_resize = xlib.XCreateFontCursor(display.handle, xlib.XC_sizing);
+ cursor_move = xlib.XCreateFontCursor(display.handle, xlib.XC_fleur);
+ _ = xlib.XDefineCursor(display.handle, display.root, cursor_normal);
+}
+
+fn setup_bars(allocator: std.mem.Allocator, display: *Display) void {
+ var current_monitor = monitor_mod.monitors;
+ var last_bar: ?*bar_mod.Bar = null;
+
+ while (current_monitor) |monitor| {
+ const bar = bar_mod.Bar.create(allocator, display.handle, display.screen, monitor, config.font);
+ if (bar) |created_bar| {
+ if (tiling.bar_height == 0) {
+ tiling.set_bar_height(created_bar.height);
+ }
+
+ if (config.blocks.items.len > 0) {
+ for (config.blocks.items) |cfg_block| {
+ const block = config_block_to_bar_block(cfg_block);
+ created_bar.add_block(block);
+ }
+ } else {
+ created_bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
+ created_bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
+ created_bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
+ }
+
+ if (last_bar) |prev| {
+ prev.next = created_bar;
+ } else {
+ bar_mod.bars = created_bar;
+ }
+ last_bar = created_bar;
+ std.debug.print("bar created for monitor {d}\n", .{monitor.num});
+ }
+ current_monitor = monitor.next;
+ }
+}
+
+fn config_block_to_bar_block(cfg: config_mod.Block) blocks_mod.Block {
+ return switch (cfg.block_type) {
+ .static => blocks_mod.Block.init_static(cfg.format, cfg.color, cfg.underline),
+ .datetime => blocks_mod.Block.init_datetime(cfg.format, cfg.datetime_format orelse "%H:%M", cfg.interval, cfg.color, cfg.underline),
+ .ram => blocks_mod.Block.init_ram(cfg.format, cfg.interval, cfg.color, cfg.underline),
+ .shell => blocks_mod.Block.init_shell(cfg.format, cfg.command orelse "", cfg.interval, cfg.color, cfg.underline),
+ .battery => blocks_mod.Block.init_battery(
+ cfg.format_charging orelse "",
+ cfg.format_discharging orelse "",
+ cfg.format_full orelse "",
+ cfg.battery_name orelse "BAT0",
+ cfg.interval,
+ cfg.color,
+ cfg.underline,
+ ),
+ .cpu_temp => blocks_mod.Block.init_cpu_temp(
+ cfg.format,
+ cfg.thermal_zone orelse "thermal_zone0",
+ cfg.interval,
+ cfg.color,
+ cfg.underline,
+ ),
+ };
+}
+
+fn setup_monitors(display: *Display) void {
+ std.debug.print("checking xinerama...\n", .{});
+ if (xlib.XineramaIsActive(display.handle) != 0) {
+ std.debug.print("xinerama is active!\n", .{});
+ var screen_count: c_int = 0;
+ const screens = xlib.XineramaQueryScreens(display.handle, &screen_count);
+
+ if (screen_count > 0 and screens != null) {
+ var prev_monitor: ?*Monitor = null;
+ var index: usize = 0;
+
+ while (index < @as(usize, @intCast(screen_count))) : (index += 1) {
+ const screen = screens[index];
+ const mon = monitor_mod.create() orelse continue;
+
+ mon.num = @intCast(index);
+ mon.mon_x = screen.x_org;
+ mon.mon_y = screen.y_org;
+ mon.mon_w = screen.width;
+ mon.mon_h = screen.height;
+ mon.win_x = screen.x_org;
+ mon.win_y = screen.y_org;
+ mon.win_w = screen.width;
+ mon.win_h = screen.height;
+ mon.lt[0] = &tiling.layout;
+ mon.lt[1] = &monocle.layout;
+ mon.lt[2] = &floating.layout;
+ mon.lt[3] = &scrolling.layout;
+ for (0..10) |i| {
+ mon.pertag.ltidxs[i][0] = mon.lt[0];
+ mon.pertag.ltidxs[i][1] = mon.lt[1];
+ mon.pertag.ltidxs[i][2] = mon.lt[2];
+ mon.pertag.ltidxs[i][3] = mon.lt[3];
+ }
+
+ if (prev_monitor) |prev| {
+ prev.next = mon;
+ } else {
+ monitor_mod.monitors = mon;
+ }
+ prev_monitor = mon;
+
+ std.debug.print("monitor {d}: {d}x{d} at ({d},{d})\n", .{ index, mon.mon_w, mon.mon_h, mon.mon_x, mon.mon_y });
+ }
+
+ monitor_mod.selected_monitor = monitor_mod.monitors;
+ _ = xlib.XFree(@ptrCast(screens));
+ return;
+ }
+ } else {
+ std.debug.print("xinerama not active, using single monitor\n", .{});
+ }
+
+ const mon = monitor_mod.create() orelse return;
+ mon.mon_x = 0;
+ mon.mon_y = 0;
+ mon.mon_w = display.screen_width();
+ mon.mon_h = display.screen_height();
+ mon.win_x = 0;
+ mon.win_y = 0;
+ mon.win_w = display.screen_width();
+ mon.win_h = display.screen_height();
+ mon.lt[0] = &tiling.layout;
+ mon.lt[1] = &monocle.layout;
+ mon.lt[2] = &floating.layout;
+ mon.lt[3] = &scrolling.layout;
+ for (0..10) |i| {
+ mon.pertag.ltidxs[i][0] = mon.lt[0];
+ mon.pertag.ltidxs[i][1] = mon.lt[1];
+ mon.pertag.ltidxs[i][2] = mon.lt[2];
+ mon.pertag.ltidxs[i][3] = mon.lt[3];
+ }
+ monitor_mod.monitors = mon;
+ monitor_mod.selected_monitor = mon;
+ std.debug.print("monitor created: {d}x{d}\n", .{ mon.mon_w, mon.mon_h });
+}
+
+fn apply_config_values() void {
+ border_color_focused = config.border_focused;
+ border_color_unfocused = config.border_unfocused;
+ border_width = config.border_width;
+ gap_inner_h = config.gap_inner_h;
+ gap_inner_v = config.gap_inner_v;
+ gap_outer_h = config.gap_outer_h;
+ gap_outer_v = config.gap_outer_v;
+ tags = config.tags;
+}
+
+fn setup_default_keybinds() void {
+ const mod_key: u32 = 1 << 6;
+ const shift_key: u32 = 1 << 0;
+ const control_key: u32 = 1 << 2;
+
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0xff0d, .action = .spawn_terminal }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'd', .action = .spawn, .str_arg = "rofi -show drun" }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 's', .action = .spawn, .str_arg = "maim -s | xclip -selection clipboard -t image/png" }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'q', .action = .kill_client }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'q', .action = .quit }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'r', .action = .reload_config }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'j', .action = .focus_next }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'k', .action = .focus_prev }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'j', .action = .move_next }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 'k', .action = .move_prev }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'h', .action = .resize_master, .int_arg = -50 }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'l', .action = .resize_master, .int_arg = 50 }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'i', .action = .inc_master }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'p', .action = .dec_master }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'a', .action = .toggle_gaps }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'f', .action = .toggle_fullscreen }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0x0020, .action = .toggle_floating }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 'n', .action = .cycle_layout }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0x002c, .action = .focus_monitor, .int_arg = -1 }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = 0x002e, .action = .focus_monitor, .int_arg = 1 }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 0x002c, .action = .send_to_monitor, .int_arg = -1 }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = 0x002e, .action = .send_to_monitor, .int_arg = 1 }) catch {};
+
+ var tag_index: i32 = 0;
+ while (tag_index < 9) : (tag_index += 1) {
+ const keysym: u64 = @as(u64, '1') + @as(u64, @intCast(tag_index));
+ config.add_keybind(.{ .mod_mask = mod_key, .keysym = keysym, .action = .view_tag, .int_arg = tag_index }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | shift_key, .keysym = keysym, .action = .move_to_tag, .int_arg = tag_index }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | control_key, .keysym = keysym, .action = .toggle_view_tag, .int_arg = tag_index }) catch {};
+ config.add_keybind(.{ .mod_mask = mod_key | control_key | shift_key, .keysym = keysym, .action = .toggle_tag, .int_arg = tag_index }) catch {};
+ }
+}
+
+fn grab_keybinds(display: *Display) void {
+ update_numlock_mask(display);
+ const modifiers = [_]c_uint{ 0, xlib.LockMask, numlock_mask, numlock_mask | xlib.LockMask };
+
+ _ = xlib.XUngrabKey(display.handle, xlib.AnyKey, xlib.AnyModifier, display.root);
+
+ for (config.keybinds.items) |keybind| {
+ const keycode = xlib.XKeysymToKeycode(display.handle, @intCast(keybind.keysym));
+ if (keycode != 0) {
+ for (modifiers) |modifier| {
+ _ = xlib.XGrabKey(
+ display.handle,
+ keycode,
+ keybind.mod_mask | modifier,
+ display.root,
+ xlib.True,
+ xlib.GrabModeAsync,
+ xlib.GrabModeAsync,
+ );
+ }
+ }
+ }
+
+ for (config.buttons.items) |button| {
+ if (button.click == .client_win) {
+ for (modifiers) |modifier| {
+ _ = xlib.XGrabButton(
+ display.handle,
+ @intCast(button.button),
+ button.mod_mask | modifier,
+ display.root,
+ xlib.True,
+ xlib.ButtonPressMask | xlib.ButtonReleaseMask | xlib.PointerMotionMask,
+ xlib.GrabModeAsync,
+ xlib.GrabModeAsync,
+ xlib.None,
+ xlib.None,
+ );
+ }
+ }
+ }
+
+ std.debug.print("grabbed {d} keybinds from config\n", .{config.keybinds.items.len});
+}
+
+fn get_state(display: *Display, window: xlib.Window) c_long {
+ var actual_type: xlib.Atom = 0;
+ var actual_format: c_int = 0;
+ var num_items: c_ulong = 0;
+ var bytes_after: c_ulong = 0;
+ var prop: [*c]u8 = null;
+
+ const result = xlib.XGetWindowProperty(
+ display.handle,
+ window,
+ wm_state,
+ 0,
+ 2,
+ xlib.False,
+ wm_state,
+ &actual_type,
+ &actual_format,
+ &num_items,
+ &bytes_after,
+ &prop,
+ );
+
+ if (result != 0 or actual_type != wm_state or num_items < 1) {
+ if (prop != null) {
+ _ = xlib.XFree(prop);
+ }
+ return WithdrawnState;
+ }
+
+ const state: c_long = @as(*c_long, @alignCast(@ptrCast(prop))).*;
+ _ = xlib.XFree(prop);
+ return state;
+}
+
+fn scan_existing_windows(display: *Display) void {
+ var root_return: xlib.Window = undefined;
+ var parent_return: xlib.Window = undefined;
+ var children: [*c]xlib.Window = undefined;
+ var num_children: c_uint = undefined;
+
+ if (xlib.XQueryTree(display.handle, display.root, &root_return, &parent_return, &children, &num_children) == 0) {
+ return;
+ }
+
+ var index: c_uint = 0;
+ while (index < num_children) : (index += 1) {
+ var window_attrs: xlib.XWindowAttributes = undefined;
+ if (xlib.XGetWindowAttributes(display.handle, children[index], &window_attrs) == 0) {
+ continue;
+ }
+ if (window_attrs.override_redirect != 0) {
+ continue;
+ }
+ var trans: xlib.Window = 0;
+ if (xlib.XGetTransientForHint(display.handle, children[index], &trans) != 0) {
+ continue;
+ }
+ if (window_attrs.map_state == IsViewable or get_state(display, children[index]) == IconicState) {
+ manage(display, children[index], &window_attrs);
+ }
+ }
+
+ index = 0;
+ while (index < num_children) : (index += 1) {
+ var window_attrs: xlib.XWindowAttributes = undefined;
+ if (xlib.XGetWindowAttributes(display.handle, children[index], &window_attrs) == 0) {
+ continue;
+ }
+ var trans: xlib.Window = 0;
+ if (xlib.XGetTransientForHint(display.handle, children[index], &trans) != 0) {
+ if (window_attrs.map_state == IsViewable or get_state(display, children[index]) == IconicState) {
+ manage(display, children[index], &window_attrs);
+ }
+ }
+ }
+
+ if (children != null) {
+ _ = xlib.XFree(@ptrCast(children));
+ }
+}
+
+fn run_event_loop(display: *Display) void {
+ const x11_fd = xlib.XConnectionNumber(display.handle);
+ var fds = [_]std.posix.pollfd{
+ .{ .fd = x11_fd, .events = std.posix.POLL.IN, .revents = 0 },
+ };
+
+ _ = xlib.XSync(display.handle, xlib.False);
+
+ while (running) {
+ while (xlib.XPending(display.handle) > 0) {
+ var event = display.next_event();
+ handle_event(display, &event);
+ }
+
+ tick_animations();
+
+ var current_bar = bar_mod.bars;
+ while (current_bar) |bar| {
+ bar.update_blocks();
+ bar.draw(display.handle, &tags);
+ current_bar = bar.next;
+ }
+
+ const poll_timeout: i32 = if (scroll_animation.is_active()) 16 else 1000;
+ _ = std.posix.poll(&fds, poll_timeout) catch 0;
+ }
+}
+
+fn handle_event(display: *Display, event: *xlib.XEvent) void {
+ const event_type = events.get_event_type(event);
+
+ if (event_type == .button_press) {
+ std.debug.print("EVENT: button_press received type={d}\n", .{event.type});
+ }
+
+ switch (event_type) {
+ .map_request => handle_map_request(display, &event.xmaprequest),
+ .configure_request => handle_configure_request(display, &event.xconfigurerequest),
+ .key_press => handle_key_press(display, &event.xkey),
+ .destroy_notify => handle_destroy_notify(display, &event.xdestroywindow),
+ .unmap_notify => handle_unmap_notify(display, &event.xunmap),
+ .enter_notify => handle_enter_notify(display, &event.xcrossing),
+ .focus_in => handle_focus_in(display, &event.xfocus),
+ .motion_notify => handle_motion_notify(display, &event.xmotion),
+ .client_message => handle_client_message(display, &event.xclient),
+ .button_press => handle_button_press(display, &event.xbutton),
+ .expose => handle_expose(display, &event.xexpose),
+ .property_notify => handle_property_notify(display, &event.xproperty),
+ else => {},
+ }
+}
+
+fn handle_map_request(display: *Display, event: *xlib.XMapRequestEvent) void {
+ std.debug.print("map_request: window=0x{x}\n", .{event.window});
+
+ var window_attributes: xlib.XWindowAttributes = undefined;
+ if (xlib.XGetWindowAttributes(display.handle, event.window, &window_attributes) == 0) {
+ return;
+ }
+ if (window_attributes.override_redirect != 0) {
+ return;
+ }
+ if (client_mod.window_to_client(event.window) != null) {
+ return;
+ }
+
+ manage(display, event.window, &window_attributes);
+}
+
+fn manage(display: *Display, win: xlib.Window, window_attrs: *xlib.XWindowAttributes) void {
+ const client = client_mod.create(win) orelse return;
+ var trans: xlib.Window = 0;
+
+ client.x = window_attrs.x;
+ client.y = window_attrs.y;
+ client.width = window_attrs.width;
+ client.height = window_attrs.height;
+ client.old_x = window_attrs.x;
+ client.old_y = window_attrs.y;
+ client.old_width = window_attrs.width;
+ client.old_height = window_attrs.height;
+ client.old_border_width = window_attrs.border_width;
+ client.border_width = border_width;
+
+ update_title(display, client);
+
+ if (xlib.XGetTransientForHint(display.handle, win, &trans) != 0) {
+ if (client_mod.window_to_client(trans)) |transient_client| {
+ client.monitor = transient_client.monitor;
+ client.tags = transient_client.tags;
+ }
+ }
+
+ if (client.monitor == null) {
+ client.monitor = monitor_mod.selected_monitor;
+ apply_rules(display, client);
+ }
+
+ const monitor = client.monitor orelse return;
+
+ if (client.x + client.width > monitor.win_x + monitor.win_w) {
+ client.x = monitor.win_x + monitor.win_w - client.width - 2 * client.border_width;
+ }
+ if (client.y + client.height > monitor.win_y + monitor.win_h) {
+ client.y = monitor.win_y + monitor.win_h - client.height - 2 * client.border_width;
+ }
+ client.x = @max(client.x, monitor.win_x);
+ client.y = @max(client.y, monitor.win_y);
+
+ _ = xlib.XSetWindowBorderWidth(display.handle, win, @intCast(client.border_width));
+ _ = xlib.XSetWindowBorder(display.handle, win, border_color_unfocused);
+ tiling.send_configure(client);
+
+ update_window_type(display, client);
+ update_size_hints(display, client);
+ update_wm_hints(display, client);
+
+ _ = xlib.XSelectInput(
+ display.handle,
+ win,
+ xlib.EnterWindowMask | xlib.FocusChangeMask | xlib.PropertyChangeMask | xlib.StructureNotifyMask,
+ );
+ grabbuttons(display, client, false);
+
+ if (!client.is_floating) {
+ client.is_floating = trans != 0 or client.is_fixed;
+ client.old_state = client.is_floating;
+ }
+ if (client.is_floating) {
+ _ = xlib.XRaiseWindow(display.handle, client.window);
+ }
+
+ client_mod.attach_aside(client);
+ client_mod.attach_stack(client);
+
+ _ = xlib.XChangeProperty(display.handle, display.root, net_client_list, xlib.XA_WINDOW, 32, xlib.PropModeAppend, @ptrCast(&client.window), 1);
+ _ = xlib.XMoveResizeWindow(display.handle, client.window, client.x + 2 * display.screen_width(), client.y, @intCast(client.width), @intCast(client.height));
+ set_client_state(display, client, NormalState);
+
+ if (client.monitor == monitor_mod.selected_monitor) {
+ const selmon = monitor_mod.selected_monitor orelse return;
+ unfocus_client(display, selmon.sel, false);
+ }
+ monitor.sel = client;
+
+ if (is_scrolling_layout(monitor)) {
+ monitor.scroll_offset = 0;
+ }
+
+ arrange(monitor);
+ _ = xlib.XMapWindow(display.handle, win);
+ focus(display, null);
+}
+
+fn handle_configure_request(display: *Display, event: *xlib.XConfigureRequestEvent) void {
+ const client = client_mod.window_to_client(event.window);
+
+ if (client) |managed_client| {
+ if ((event.value_mask & xlib.c.CWBorderWidth) != 0) {
+ managed_client.border_width = event.border_width;
+ } else if (managed_client.is_floating or (managed_client.monitor != null and managed_client.monitor.?.lt[managed_client.monitor.?.sel_lt] == null)) {
+ const monitor = managed_client.monitor orelse return;
+ if ((event.value_mask & xlib.c.CWX) != 0) {
+ managed_client.old_x = managed_client.x;
+ managed_client.x = monitor.mon_x + event.x;
+ }
+ if ((event.value_mask & xlib.c.CWY) != 0) {
+ managed_client.old_y = managed_client.y;
+ managed_client.y = monitor.mon_y + event.y;
+ }
+ if ((event.value_mask & xlib.c.CWWidth) != 0) {
+ managed_client.old_width = managed_client.width;
+ managed_client.width = event.width;
+ }
+ if ((event.value_mask & xlib.c.CWHeight) != 0) {
+ managed_client.old_height = managed_client.height;
+ managed_client.height = event.height;
+ }
+ const client_full_width = managed_client.width + managed_client.border_width * 2;
+ const client_full_height = managed_client.height + managed_client.border_width * 2;
+ if ((managed_client.x + managed_client.width) > monitor.mon_x + monitor.mon_w and managed_client.is_floating) {
+ managed_client.x = monitor.mon_x + @divTrunc(monitor.mon_w, 2) - @divTrunc(client_full_width, 2);
+ }
+ if ((managed_client.y + managed_client.height) > monitor.mon_y + monitor.mon_h and managed_client.is_floating) {
+ managed_client.y = monitor.mon_y + @divTrunc(monitor.mon_h, 2) - @divTrunc(client_full_height, 2);
+ }
+ if (((event.value_mask & (xlib.c.CWX | xlib.c.CWY)) != 0) and ((event.value_mask & (xlib.c.CWWidth | xlib.c.CWHeight)) == 0)) {
+ tiling.send_configure(managed_client);
+ }
+ if (client_mod.is_visible(managed_client)) {
+ _ = xlib.XMoveResizeWindow(display.handle, managed_client.window, managed_client.x, managed_client.y, @intCast(managed_client.width), @intCast(managed_client.height));
+ }
+ } else {
+ tiling.send_configure(managed_client);
+ }
+ } else {
+ var changes: xlib.XWindowChanges = undefined;
+ changes.x = event.x;
+ changes.y = event.y;
+ changes.width = event.width;
+ changes.height = event.height;
+ changes.border_width = event.border_width;
+ changes.sibling = event.above;
+ changes.stack_mode = event.detail;
+ _ = xlib.XConfigureWindow(display.handle, event.window, @intCast(event.value_mask), &changes);
+ }
+ _ = xlib.XSync(display.handle, xlib.False);
+}
+
+fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
+ const keysym = xlib.XKeycodeToKeysym(display.handle, @intCast(event.keycode), 0);
+ const clean_state = event.state & ~@as(c_uint, xlib.LockMask | xlib.Mod2Mask);
+
+ for (config.keybinds.items) |keybind| {
+ if (keysym == keybind.keysym and clean_state == keybind.mod_mask) {
+ execute_action(display, keybind.action, keybind.int_arg, keybind.str_arg);
+ return;
+ }
+ }
+}
+
+fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, str_arg: ?[]const u8) void {
+ switch (action) {
+ .spawn_terminal => spawn_terminal(),
+ .spawn => {
+ if (str_arg) |cmd| {
+ spawn_command(cmd);
+ }
+ },
+ .kill_client => kill_focused(display),
+ .quit => {
+ std.debug.print("quit keybind pressed\n", .{});
+ running = false;
+ },
+ .reload_config => reload_config(display),
+ .restart => {},
+ .focus_next => focusstack(display, 1),
+ .focus_prev => focusstack(display, -1),
+ .move_next => movestack(display, 1),
+ .move_prev => movestack(display, -1),
+ .resize_master => setmfact(@as(f32, @floatFromInt(int_arg)) / 1000.0),
+ .inc_master => incnmaster(1),
+ .dec_master => incnmaster(-1),
+ .toggle_floating => toggle_floating(display),
+ .toggle_fullscreen => toggle_fullscreen(display),
+ .toggle_gaps => toggle_gaps(),
+ .cycle_layout => cycle_layout(),
+ .set_layout => set_layout(str_arg),
+ .set_layout_tiling => {},
+ .set_layout_floating => {},
+ .view_tag => {
+ const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
+ view(display, tag_mask);
+ },
+ .view_next_tag => view_adjacent_tag(display, 1),
+ .view_prev_tag => view_adjacent_tag(display, -1),
+ .view_next_nonempty_tag => view_adjacent_nonempty_tag(display, 1),
+ .view_prev_nonempty_tag => view_adjacent_nonempty_tag(display, -1),
+ .move_to_tag => {
+ const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
+ tag_client(display, tag_mask);
+ },
+ .toggle_view_tag => {
+ const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
+ toggle_view(display, tag_mask);
+ },
+ .toggle_tag => {
+ const tag_mask: u32 = @as(u32, 1) << @intCast(int_arg);
+ toggle_client_tag(display, tag_mask);
+ },
+ .focus_monitor => focusmon(display, int_arg),
+ .send_to_monitor => sendmon(display, int_arg),
+ .scroll_left => {
+ scroll_layout(-1);
+ },
+ .scroll_right => {
+ scroll_layout(1);
+ },
+ }
+}
+
+fn reload_config(display: *Display) void {
+ std.debug.print("reloading config...\n", .{});
+
+ ungrab_keybinds(display);
+
+ config.keybinds.clearRetainingCapacity();
+ config.rules.clearRetainingCapacity();
+ config.blocks.clearRetainingCapacity();
+
+ lua.deinit();
+ _ = lua.init(&config);
+
+ const loaded = if (config_path_global) |path|
+ lua.load_file(path)
+ else
+ lua.load_config();
+
+ if (loaded) {
+ if (config_path_global) |path| {
+ std.debug.print("reloaded config from {s}\n", .{path});
+ } else {
+ std.debug.print("reloaded config from ~/.config/oxwm/config.lua\n", .{});
+ }
+ apply_config_values();
+ } else {
+ std.debug.print("reload failed, restoring defaults\n", .{});
+ setup_default_keybinds();
+ }
+
+ bar_mod.destroy_bars(gpa.allocator(), display.handle);
+ setup_bars(gpa.allocator(), display);
+ rebuild_bar_blocks();
+
+ grab_keybinds(display);
+}
+
+fn rebuild_bar_blocks() void {
+ var current_bar = bar_mod.bars;
+ while (current_bar) |bar| {
+ bar.clear_blocks();
+ if (config.blocks.items.len > 0) {
+ for (config.blocks.items) |cfg_block| {
+ const block = config_block_to_bar_block(cfg_block);
+ bar.add_block(block);
+ }
+ } else {
+ bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
+ bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
+ bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
+ }
+ current_bar = bar.next;
+ }
+}
+
+fn ungrab_keybinds(display: *Display) void {
+ _ = xlib.XUngrabKey(display.handle, xlib.AnyKey, xlib.AnyModifier, display.root);
+}
+
+fn spawn_child_setup() void {
+ _ = std.c.setsid();
+ if (display_global) |display| {
+ const display_fd = xlib.XConnectionNumber(display.handle);
+ std.posix.close(@intCast(display_fd));
+ }
+ const sigchld_handler = std.posix.Sigaction{
+ .handler = .{ .handler = std.posix.SIG.DFL },
+ .mask = std.mem.zeroes(std.posix.sigset_t),
+ .flags = 0,
+ };
+ std.posix.sigaction(std.posix.SIG.CHLD, &sigchld_handler, null);
+}
+
+fn spawn_command(cmd: []const u8) void {
+ const pid = std.posix.fork() catch return;
+ if (pid == 0) {
+ spawn_child_setup();
+ var cmd_buf: [1024]u8 = undefined;
+ if (cmd.len >= cmd_buf.len) {
+ std.posix.exit(1);
+ }
+ @memcpy(cmd_buf[0..cmd.len], cmd);
+ cmd_buf[cmd.len] = 0;
+ const argv = [_:null]?[*:0]const u8{ "sh", "-c", @ptrCast(&cmd_buf) };
+ _ = std.posix.execvpeZ("sh", &argv, std.c.environ) catch {};
+ std.posix.exit(1);
+ }
+}
+
+fn spawn_terminal() void {
+ const pid = std.posix.fork() catch return;
+ if (pid == 0) {
+ spawn_child_setup();
+ var term_buf: [256]u8 = undefined;
+ const terminal = config.terminal;
+ if (terminal.len >= term_buf.len) {
+ std.posix.exit(1);
+ }
+ @memcpy(term_buf[0..terminal.len], terminal);
+ term_buf[terminal.len] = 0;
+ const term_ptr: [*:0]const u8 = @ptrCast(&term_buf);
+ const argv = [_:null]?[*:0]const u8{term_ptr};
+ _ = std.posix.execvpeZ(term_ptr, &argv, std.c.environ) catch {};
+ std.posix.exit(1);
+ }
+}
+
+fn movestack(display: *Display, direction: i32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const current = monitor.sel orelse return;
+
+ if (current.is_floating) {
+ return;
+ }
+
+ var target: ?*Client = null;
+
+ if (direction > 0) {
+ target = current.next;
+ while (target) |client| {
+ if (client_mod.is_visible(client) and !client.is_floating) {
+ break;
+ }
+ target = client.next;
+ }
+ if (target == null) {
+ target = monitor.clients;
+ while (target) |client| {
+ if (client == current) {
+ break;
+ }
+ if (client_mod.is_visible(client) and !client.is_floating) {
+ break;
+ }
+ target = client.next;
+ }
+ }
+ } else {
+ var prev: ?*Client = null;
+ var iter = monitor.clients;
+ while (iter) |client| {
+ if (client == current) {
+ break;
+ }
+ if (client_mod.is_visible(client) and !client.is_floating) {
+ prev = client;
+ }
+ iter = client.next;
+ }
+ if (prev == null) {
+ iter = current.next;
+ while (iter) |client| {
+ if (client_mod.is_visible(client) and !client.is_floating) {
+ prev = client;
+ }
+ iter = client.next;
+ }
+ }
+ target = prev;
+ }
+
+ if (target) |swap_client| {
+ if (swap_client != current) {
+ client_mod.swap_clients(current, swap_client);
+ arrange(monitor);
+ focus(display, current);
+ }
+ }
+}
+
+fn toggle_view(display: *Display, tag_mask: u32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const new_tags = monitor.tagset[monitor.sel_tags] ^ tag_mask;
+ if (new_tags != 0) {
+ monitor.tagset[monitor.sel_tags] = new_tags;
+
+ if (new_tags == ~@as(u32, 0)) {
+ monitor.pertag.prevtag = monitor.pertag.curtag;
+ monitor.pertag.curtag = 0;
+ }
+
+ if ((new_tags & (@as(u32, 1) << @intCast(monitor.pertag.curtag -| 1))) == 0) {
+ monitor.pertag.prevtag = monitor.pertag.curtag;
+ var i: u32 = 0;
+ while (i < 9) : (i += 1) {
+ if ((new_tags & (@as(u32, 1) << @intCast(i))) != 0) break;
+ }
+ monitor.pertag.curtag = i + 1;
+ }
+
+ monitor.nmaster = monitor.pertag.nmasters[monitor.pertag.curtag];
+ monitor.mfact = monitor.pertag.mfacts[monitor.pertag.curtag];
+ monitor.sel_lt = monitor.pertag.sellts[monitor.pertag.curtag];
+
+ focus_top_client(display, monitor);
+ arrange(monitor);
+ bar_mod.invalidate_bars();
+ }
+}
+
+fn toggle_client_tag(display: *Display, tag_mask: u32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const client = monitor.sel orelse return;
+ const new_tags = client.tags ^ tag_mask;
+ if (new_tags != 0) {
+ client.tags = new_tags;
+ focus_top_client(display, monitor);
+ arrange(monitor);
+ bar_mod.invalidate_bars();
+ }
+}
+
+fn toggle_gaps() void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ if (monitor.gap_inner_h == 0) {
+ monitor.gap_inner_h = gap_inner_v;
+ monitor.gap_inner_v = gap_inner_v;
+ monitor.gap_outer_h = gap_outer_h;
+ monitor.gap_outer_v = gap_outer_v;
+ } else {
+ monitor.gap_inner_h = 0;
+ monitor.gap_inner_v = 0;
+ monitor.gap_outer_h = 0;
+ monitor.gap_outer_v = 0;
+ }
+ arrange(monitor);
+}
+
+fn kill_focused(display: *Display) void {
+ const selected = monitor_mod.selected_monitor orelse return;
+ const client = selected.sel orelse return;
+ std.debug.print("killing window: 0x{x}\n", .{client.window});
+
+ if (!send_event(display, client, wm_delete)) {
+ _ = xlib.XGrabServer(display.handle);
+ _ = xlib.XKillClient(display.handle, client.window);
+ _ = xlib.XSync(display.handle, xlib.False);
+ _ = xlib.XUngrabServer(display.handle);
+ }
+}
+
+fn toggle_fullscreen(display: *Display) void {
+ const selected = monitor_mod.selected_monitor orelse return;
+ const client = selected.sel orelse return;
+ set_fullscreen(display, client, !client.is_fullscreen);
+}
+
+fn set_fullscreen(display: *Display, client: *Client, fullscreen: bool) void {
+ const monitor = client.monitor orelse return;
+
+ if (fullscreen and !client.is_fullscreen) {
+ var fullscreen_atom = net_wm_state_fullscreen;
+ _ = xlib.XChangeProperty(
+ display.handle,
+ client.window,
+ net_wm_state,
+ xlib.XA_ATOM,
+ 32,
+ xlib.PropModeReplace,
+ @ptrCast(&fullscreen_atom),
+ 1,
+ );
+ client.is_fullscreen = true;
+ client.old_state = client.is_floating;
+ client.old_border_width = client.border_width;
+ client.border_width = 0;
+ client.is_floating = true;
+
+ _ = xlib.XSetWindowBorderWidth(display.handle, client.window, 0);
+ tiling.resize_client(client, monitor.mon_x, monitor.mon_y, monitor.mon_w, monitor.mon_h);
+ _ = xlib.XRaiseWindow(display.handle, client.window);
+
+ std.debug.print("fullscreen enabled: window=0x{x}\n", .{client.window});
+ } else if (!fullscreen and client.is_fullscreen) {
+ var no_atom: xlib.Atom = 0;
+ _ = xlib.XChangeProperty(
+ display.handle,
+ client.window,
+ net_wm_state,
+ xlib.XA_ATOM,
+ 32,
+ xlib.PropModeReplace,
+ @ptrCast(&no_atom),
+ 0,
+ );
+ client.is_fullscreen = false;
+ client.is_floating = client.old_state;
+ client.border_width = client.old_border_width;
+
+ client.x = client.old_x;
+ client.y = client.old_y;
+ client.width = client.old_width;
+ client.height = client.old_height;
+
+ tiling.resize_client(client, client.x, client.y, client.width, client.height);
+ arrange(monitor);
+
+ std.debug.print("fullscreen disabled: window=0x{x}\n", .{client.window});
+ }
+}
+
+fn view(display: *Display, tag_mask: u32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ if (tag_mask == monitor.tagset[monitor.sel_tags]) {
+ return;
+ }
+ monitor.sel_tags ^= 1;
+ if (tag_mask != 0) {
+ monitor.tagset[monitor.sel_tags] = tag_mask;
+ monitor.pertag.prevtag = monitor.pertag.curtag;
+
+ if (tag_mask == ~@as(u32, 0)) {
+ monitor.pertag.curtag = 0;
+ } else {
+ var i: u32 = 0;
+ while (i < 9) : (i += 1) {
+ if ((tag_mask & (@as(u32, 1) << @intCast(i))) != 0) break;
+ }
+ monitor.pertag.curtag = i + 1;
+ }
+ } else {
+ const tmp = monitor.pertag.prevtag;
+ monitor.pertag.prevtag = monitor.pertag.curtag;
+ monitor.pertag.curtag = tmp;
+ }
+
+ monitor.nmaster = monitor.pertag.nmasters[monitor.pertag.curtag];
+ monitor.mfact = monitor.pertag.mfacts[monitor.pertag.curtag];
+ monitor.sel_lt = monitor.pertag.sellts[monitor.pertag.curtag];
+
+ focus_top_client(display, monitor);
+ arrange(monitor);
+ bar_mod.invalidate_bars();
+ std.debug.print("view: tag_mask={d}\n", .{monitor.tagset[monitor.sel_tags]});
+}
+
+fn view_adjacent_tag(display: *Display, direction: i32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const current_tag = monitor.pertag.curtag;
+ var new_tag: i32 = @intCast(current_tag);
+
+ new_tag += direction;
+ if (new_tag < 1) new_tag = 9;
+ if (new_tag > 9) new_tag = 1;
+
+ const tag_mask: u32 = @as(u32, 1) << @intCast(new_tag - 1);
+ view(display, tag_mask);
+}
+
+fn view_adjacent_nonempty_tag(display: *Display, direction: i32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const current_tag = monitor.pertag.curtag;
+ var new_tag: i32 = @intCast(current_tag);
+
+ var attempts: i32 = 0;
+ while (attempts < 9) : (attempts += 1) {
+ new_tag += direction;
+ if (new_tag < 1) new_tag = 9;
+ if (new_tag > 9) new_tag = 1;
+
+ const tag_mask: u32 = @as(u32, 1) << @intCast(new_tag - 1);
+ if (has_clients_on_tag(monitor, tag_mask)) {
+ view(display, tag_mask);
+ return;
+ }
+ }
+}
+
+fn has_clients_on_tag(monitor: *monitor_mod.Monitor, tag_mask: u32) bool {
+ var client = monitor.clients;
+ while (client) |c| {
+ if ((c.tags & tag_mask) != 0) {
+ return true;
+ }
+ client = c.next;
+ }
+ return false;
+}
+
+fn tag_client(display: *Display, tag_mask: u32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const client = monitor.sel orelse return;
+ if (tag_mask == 0) {
+ return;
+ }
+ client.tags = tag_mask;
+ focus_top_client(display, monitor);
+ arrange(monitor);
+ bar_mod.invalidate_bars();
+ std.debug.print("tag_client: window=0x{x} tag_mask={d}\n", .{ client.window, tag_mask });
+}
+
+fn focus_top_client(display: *Display, monitor: *Monitor) void {
+ var visible_client = monitor.stack;
+ while (visible_client) |client| {
+ if (client_mod.is_visible(client)) {
+ focus(display, client);
+ return;
+ }
+ visible_client = client.stack_next;
+ }
+ monitor.sel = null;
+ _ = xlib.XSetInputFocus(display.handle, display.root, xlib.RevertToPointerRoot, xlib.CurrentTime);
+}
+
+fn focusstack(display: *Display, direction: i32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const current = monitor.sel orelse return;
+
+ var next_client: ?*Client = null;
+
+ if (direction > 0) {
+ next_client = current.next;
+ while (next_client) |client| {
+ if (client_mod.is_visible(client)) {
+ break;
+ }
+ next_client = client.next;
+ }
+ if (next_client == null) {
+ next_client = monitor.clients;
+ while (next_client) |client| {
+ if (client_mod.is_visible(client)) {
+ break;
+ }
+ next_client = client.next;
+ }
+ }
+ } else {
+ var prev: ?*Client = null;
+ var iter = monitor.clients;
+ while (iter) |client| {
+ if (client == current) {
+ break;
+ }
+ if (client_mod.is_visible(client)) {
+ prev = client;
+ }
+ iter = client.next;
+ }
+ if (prev == null) {
+ iter = current.next;
+ while (iter) |client| {
+ if (client_mod.is_visible(client)) {
+ prev = client;
+ }
+ iter = client.next;
+ }
+ }
+ next_client = prev;
+ }
+
+ if (next_client) |client| {
+ focus(display, client);
+ if (client.monitor) |client_monitor| {
+ restack(display, client_monitor);
+ }
+ }
+}
+
+fn toggle_floating(_: *Display) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const client = monitor.sel orelse return;
+
+ if (client.is_fullscreen) {
+ return;
+ }
+
+ client.is_floating = !client.is_floating;
+
+ if (client.is_floating) {
+ tiling.resize(client, client.x, client.y, client.width, client.height, false);
+ }
+
+ arrange(monitor);
+ std.debug.print("toggle_floating: window=0x{x} floating={}\n", .{ client.window, client.is_floating });
+}
+
+fn incnmaster(delta: i32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const new_val = @max(0, monitor.nmaster + delta);
+ monitor.nmaster = new_val;
+ monitor.pertag.nmasters[monitor.pertag.curtag] = new_val;
+ arrange(monitor);
+ std.debug.print("incnmaster: nmaster={d}\n", .{monitor.nmaster});
+}
+
+fn setmfact(delta: f32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const new_mfact = monitor.mfact + delta;
+ if (new_mfact < 0.05 or new_mfact > 0.95) {
+ return;
+ }
+ monitor.mfact = new_mfact;
+ monitor.pertag.mfacts[monitor.pertag.curtag] = new_mfact;
+ arrange(monitor);
+ std.debug.print("setmfact: mfact={d:.2}\n", .{monitor.mfact});
+}
+
+fn cycle_layout() void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const new_lt = (monitor.sel_lt + 1) % 4;
+ monitor.sel_lt = new_lt;
+ monitor.pertag.sellts[monitor.pertag.curtag] = new_lt;
+ if (new_lt != 3) {
+ monitor.scroll_offset = 0;
+ }
+ arrange(monitor);
+ bar_mod.invalidate_bars();
+ if (monitor.lt[monitor.sel_lt]) |layout| {
+ std.debug.print("cycle_layout: {s}\n", .{layout.symbol});
+ }
+}
+
+fn set_layout(layout_name: ?[]const u8) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const name = layout_name orelse return;
+
+ const new_lt: u32 = if (std.mem.eql(u8, name, "tiling") or std.mem.eql(u8, name, "[]="))
+ 0
+ else if (std.mem.eql(u8, name, "monocle") or std.mem.eql(u8, name, "[M]"))
+ 1
+ else if (std.mem.eql(u8, name, "floating") or std.mem.eql(u8, name, "><>"))
+ 2
+ else if (std.mem.eql(u8, name, "scrolling") or std.mem.eql(u8, name, "[S]"))
+ 3
+ else {
+ std.debug.print("set_layout: unknown layout '{s}'\n", .{name});
+ return;
+ };
+
+ monitor.sel_lt = new_lt;
+ monitor.pertag.sellts[monitor.pertag.curtag] = new_lt;
+ if (new_lt != 3) {
+ monitor.scroll_offset = 0;
+ }
+ arrange(monitor);
+ bar_mod.invalidate_bars();
+ if (monitor.lt[monitor.sel_lt]) |layout| {
+ std.debug.print("set_layout: {s}\n", .{layout.symbol});
+ }
+}
+
+fn focusmon(display: *Display, direction: i32) void {
+ const selmon = monitor_mod.selected_monitor orelse return;
+ const target = monitor_mod.dir_to_monitor(direction) orelse return;
+ if (target == selmon) {
+ return;
+ }
+ unfocus_client(display, selmon.sel, false);
+ monitor_mod.selected_monitor = target;
+ focus(display, null);
+ std.debug.print("focusmon: monitor {d}\n", .{target.num});
+}
+
+fn sendmon(display: *Display, direction: i32) void {
+ const source_monitor = monitor_mod.selected_monitor orelse return;
+ const client = source_monitor.sel orelse return;
+ const target = monitor_mod.dir_to_monitor(direction) orelse return;
+
+ if (target == source_monitor) {
+ return;
+ }
+
+ client_mod.detach(client);
+ client_mod.detach_stack(client);
+ client.monitor = target;
+ client.tags = target.tagset[target.sel_tags];
+ client_mod.attach_aside(client);
+ client_mod.attach_stack(client);
+
+ focus_top_client(display, source_monitor);
+ arrange(source_monitor);
+ arrange(target);
+
+ std.debug.print("sendmon: window=0x{x} to monitor {d}\n", .{ client.window, target.num });
+}
+
+fn snap_x(client: *Client, new_x: i32, monitor: *Monitor) i32 {
+ const client_width = client.width + 2 * client.border_width;
+ if (@abs(monitor.win_x - new_x) < snap_distance) {
+ return monitor.win_x;
+ } else if (@abs((monitor.win_x + monitor.win_w) - (new_x + client_width)) < snap_distance) {
+ return monitor.win_x + monitor.win_w - client_width;
+ }
+ return new_x;
+}
+
+fn snap_y(client: *Client, new_y: i32, monitor: *Monitor) i32 {
+ const client_height = client.height + 2 * client.border_width;
+ if (@abs(monitor.win_y - new_y) < snap_distance) {
+ return monitor.win_y;
+ } else if (@abs((monitor.win_y + monitor.win_h) - (new_y + client_height)) < snap_distance) {
+ return monitor.win_y + monitor.win_h - client_height;
+ }
+ return new_y;
+}
+
+fn movemouse(display: *Display) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const client = monitor.sel orelse return;
+
+ if (client.is_fullscreen) {
+ return;
+ }
+
+ restack(display, monitor);
+
+ const was_floating = client.is_floating;
+ if (!client.is_floating) {
+ client.is_floating = true;
+ }
+
+ var root_x: c_int = undefined;
+ var root_y: c_int = undefined;
+ var dummy_win: xlib.Window = undefined;
+ var dummy_int: c_int = undefined;
+ var dummy_uint: c_uint = undefined;
+
+ _ = xlib.XQueryPointer(display.handle, display.root, &dummy_win, &dummy_win, &root_x, &root_y, &dummy_int, &dummy_int, &dummy_uint);
+
+ const grab_result = xlib.XGrabPointer(
+ display.handle,
+ display.root,
+ xlib.False,
+ xlib.ButtonPressMask | xlib.ButtonReleaseMask | xlib.PointerMotionMask,
+ xlib.GrabModeAsync,
+ xlib.GrabModeAsync,
+ xlib.None,
+ cursor_move,
+ xlib.CurrentTime,
+ );
+
+ if (grab_result != xlib.GrabSuccess) {
+ return;
+ }
+
+ const start_x = client.x;
+ const start_y = client.y;
+ const pointer_start_x = root_x;
+ const pointer_start_y = root_y;
+ var last_time: c_ulong = 0;
+
+ var event: xlib.XEvent = undefined;
+ var done = false;
+
+ while (!done) {
+ _ = xlib.XNextEvent(display.handle, &event);
+
+ switch (event.type) {
+ xlib.MotionNotify => {
+ const motion = &event.xmotion;
+ if ((motion.time - last_time) < (1000 / 60)) {
+ continue;
+ }
+ last_time = motion.time;
+ const delta_x = motion.x_root - pointer_start_x;
+ const delta_y = motion.y_root - pointer_start_y;
+ var new_x = start_x + delta_x;
+ var new_y = start_y + delta_y;
+ if (client.monitor) |client_monitor| {
+ new_x = snap_x(client, new_x, client_monitor);
+ new_y = snap_y(client, new_y, client_monitor);
+ }
+ tiling.resize(client, new_x, new_y, client.width, client.height, true);
+ },
+ xlib.ButtonRelease => {
+ done = true;
+ },
+ else => {},
+ }
+ }
+
+ _ = xlib.XUngrabPointer(display.handle, xlib.CurrentTime);
+
+ const new_mon = monitor_mod.rect_to_monitor(client.x, client.y, client.width, client.height);
+ if (new_mon != null and new_mon != monitor) {
+ client_mod.detach(client);
+ client_mod.detach_stack(client);
+ client.monitor = new_mon;
+ client.tags = new_mon.?.tagset[new_mon.?.sel_tags];
+ client_mod.attach_aside(client);
+ client_mod.attach_stack(client);
+ monitor_mod.selected_monitor = new_mon;
+ focus(display, client);
+ arrange(monitor);
+ arrange(new_mon.?);
+ } else {
+ arrange(monitor);
+ }
+
+ if (config.auto_tile and !was_floating) {
+ const drop_monitor = client.monitor orelse return;
+ const center_x = client.x + @divTrunc(client.width, 2);
+ const center_y = client.y + @divTrunc(client.height, 2);
+
+ if (client_mod.tiled_window_at(client, drop_monitor, center_x, center_y)) |target| {
+ client_mod.insert_before(client, target);
+ }
+
+ client.is_floating = false;
+ arrange(drop_monitor);
+ }
+}
+
+fn resizemouse(display: *Display) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ const client = monitor.sel orelse return;
+
+ if (client.is_fullscreen) {
+ return;
+ }
+
+ restack(display, monitor);
+
+ if (!client.is_floating) {
+ client.is_floating = true;
+ }
+
+ const grab_result = xlib.XGrabPointer(
+ display.handle,
+ display.root,
+ xlib.False,
+ xlib.ButtonPressMask | xlib.ButtonReleaseMask | xlib.PointerMotionMask,
+ xlib.GrabModeAsync,
+ xlib.GrabModeAsync,
+ xlib.None,
+ cursor_resize,
+ xlib.CurrentTime,
+ );
+
+ if (grab_result != xlib.GrabSuccess) {
+ return;
+ }
+
+ _ = xlib.XWarpPointer(display.handle, xlib.None, client.window, 0, 0, 0, 0, client.width + client.border_width - 1, client.height + client.border_width - 1);
+
+ var event: xlib.XEvent = undefined;
+ var done = false;
+ var last_time: c_ulong = 0;
+
+ while (!done) {
+ _ = xlib.XNextEvent(display.handle, &event);
+
+ switch (event.type) {
+ xlib.MotionNotify => {
+ const motion = &event.xmotion;
+ if ((motion.time - last_time) < (1000 / 60)) {
+ continue;
+ }
+ last_time = motion.time;
+ var new_width = @max(1, motion.x_root - client.x - 2 * client.border_width + 1);
+ var new_height = @max(1, motion.y_root - client.y - 2 * client.border_width + 1);
+ if (client.monitor) |client_monitor| {
+ const client_right = client.x + new_width + 2 * client.border_width;
+ const client_bottom = client.y + new_height + 2 * client.border_width;
+ const mon_right = client_monitor.win_x + client_monitor.win_w;
+ const mon_bottom = client_monitor.win_y + client_monitor.win_h;
+ if (@abs(mon_right - client_right) < snap_distance) {
+ new_width = @max(1, mon_right - client.x - 2 * client.border_width);
+ }
+ if (@abs(mon_bottom - client_bottom) < snap_distance) {
+ new_height = @max(1, mon_bottom - client.y - 2 * client.border_width);
+ }
+ }
+ tiling.resize(client, client.x, client.y, new_width, new_height, true);
+ },
+ xlib.ButtonRelease => {
+ done = true;
+ },
+ else => {},
+ }
+ }
+
+ _ = xlib.XUngrabPointer(display.handle, xlib.CurrentTime);
+ arrange(monitor);
+}
+
+fn handle_expose(display: *Display, event: *xlib.XExposeEvent) void {
+ if (event.count != 0) return;
+
+ if (bar_mod.window_to_bar(event.window)) |bar| {
+ bar.invalidate();
+ bar.draw(display.handle, &tags);
+ }
+}
+
+fn clean_mask(mask: c_uint) c_uint {
+ const lock: c_uint = @intCast(xlib.LockMask);
+ const shift: c_uint = @intCast(xlib.ShiftMask);
+ const ctrl: c_uint = @intCast(xlib.ControlMask);
+ const mod1: c_uint = @intCast(xlib.Mod1Mask);
+ const mod2: c_uint = @intCast(xlib.Mod2Mask);
+ const mod3: c_uint = @intCast(xlib.Mod3Mask);
+ const mod4: c_uint = @intCast(xlib.Mod4Mask);
+ const mod5: c_uint = @intCast(xlib.Mod5Mask);
+ return mask & ~(lock | numlock_mask) & (shift | ctrl | mod1 | mod2 | mod3 | mod4 | mod5);
+}
+
+fn handle_button_press(display: *Display, event: *xlib.XButtonEvent) void {
+ std.debug.print("button_press: window=0x{x} subwindow=0x{x}\n", .{ event.window, event.subwindow });
+
+ const clicked_monitor = monitor_mod.window_to_monitor(event.window);
+ if (clicked_monitor) |monitor| {
+ if (monitor != monitor_mod.selected_monitor) {
+ if (monitor_mod.selected_monitor) |selmon| {
+ unfocus_client(display, selmon.sel, true);
+ }
+ monitor_mod.selected_monitor = monitor;
+ focus(display, null);
+ }
+ }
+
+ if (bar_mod.window_to_bar(event.window)) |bar| {
+ const clicked_tag = bar.handle_click(event.x, &tags);
+ if (clicked_tag) |tag_index| {
+ const tag_mask: u32 = @as(u32, 1) << @intCast(tag_index);
+ view(display, tag_mask);
+ }
+ return;
+ }
+
+ const click_client = client_mod.window_to_client(event.window);
+ if (click_client) |found_client| {
+ focus(display, found_client);
+ if (monitor_mod.selected_monitor) |selmon| {
+ restack(display, selmon);
+ }
+ _ = xlib.XAllowEvents(display.handle, xlib.ReplayPointer, xlib.CurrentTime);
+ }
+
+ const clean_state = clean_mask(event.state);
+ for (config.buttons.items) |button| {
+ if (button.click != .client_win) continue;
+ const button_clean_mask = clean_mask(button.mod_mask);
+ if (clean_state == button_clean_mask and event.button == button.button) {
+ switch (button.action) {
+ .move_mouse => movemouse(display),
+ .resize_mouse => resizemouse(display),
+ .toggle_floating => {
+ if (click_client) |found_client| {
+ found_client.is_floating = !found_client.is_floating;
+ if (monitor_mod.selected_monitor) |monitor| {
+ arrange(monitor);
+ }
+ }
+ },
+ }
+ return;
+ }
+ }
+}
+
+fn handle_client_message(display: *Display, event: *xlib.XClientMessageEvent) void {
+ const client = client_mod.window_to_client(event.window) orelse return;
+
+ if (event.message_type == net_wm_state) {
+ const action = event.data.l[0];
+ const first_property = @as(xlib.Atom, @intCast(event.data.l[1]));
+ const second_property = @as(xlib.Atom, @intCast(event.data.l[2]));
+
+ if (first_property == net_wm_state_fullscreen or second_property == net_wm_state_fullscreen) {
+ const net_wm_state_remove = 0;
+ const net_wm_state_add = 1;
+ const net_wm_state_toggle = 2;
+
+ if (action == net_wm_state_add) {
+ set_fullscreen(display, client, true);
+ } else if (action == net_wm_state_remove) {
+ set_fullscreen(display, client, false);
+ } else if (action == net_wm_state_toggle) {
+ set_fullscreen(display, client, !client.is_fullscreen);
+ }
+ }
+ } else if (event.message_type == net_active_window) {
+ const selected = monitor_mod.selected_monitor orelse return;
+ if (client != selected.sel and !client.is_urgent) {
+ set_urgent(display, client, true);
+ }
+ }
+}
+
+fn handle_destroy_notify(display: *Display, event: *xlib.XDestroyWindowEvent) void {
+ const client = client_mod.window_to_client(event.window) orelse return;
+ std.debug.print("destroy_notify: window=0x{x}\n", .{event.window});
+ unmanage(display, client);
+}
+
+fn handle_unmap_notify(display: *Display, event: *xlib.XUnmapEvent) void {
+ const client = client_mod.window_to_client(event.window) orelse return;
+ std.debug.print("unmap_notify: window=0x{x}\n", .{event.window});
+ unmanage(display, client);
+}
+
+fn unmanage(display: *Display, client: *Client) void {
+ const client_monitor = client.monitor;
+
+ var next_focus: ?*Client = null;
+ if (client_monitor) |monitor| {
+ if (monitor.sel == client and is_scrolling_layout(monitor)) {
+ next_focus = client.next;
+ if (next_focus == null) {
+ var prev: ?*Client = null;
+ var iter = monitor.clients;
+ while (iter) |c| {
+ if (c == client) break;
+ prev = c;
+ iter = c.next;
+ }
+ next_focus = prev;
+ }
+ }
+ }
+
+ client_mod.detach(client);
+ client_mod.detach_stack(client);
+
+ if (client_monitor) |monitor| {
+ if (monitor.sel == client) {
+ monitor.sel = if (next_focus) |nf| nf else monitor.stack;
+ }
+ if (is_scrolling_layout(monitor)) {
+ const target = if (monitor.sel) |sel| scrolling.get_target_scroll_for_window(monitor, sel) else 0;
+ if (target == 0) {
+ monitor.scroll_offset = scrolling.get_scroll_step(monitor);
+ } else {
+ monitor.scroll_offset = 0;
+ }
+ }
+ arrange(monitor);
+ }
+
+ if (client_monitor) |monitor| {
+ if (monitor.sel) |selected| {
+ focus(display, selected);
+ }
+ }
+
+ client_mod.destroy(client);
+ update_client_list(display);
+ bar_mod.invalidate_bars();
+}
+
+fn handle_enter_notify(display: *Display, event: *xlib.XCrossingEvent) void {
+ if ((event.mode != xlib.NotifyNormal or event.detail == xlib.NotifyInferior) and event.window != display.root) {
+ return;
+ }
+
+ const client = client_mod.window_to_client(event.window);
+ const target_mon = if (client) |c| c.monitor else monitor_mod.window_to_monitor(event.window);
+ const selmon = monitor_mod.selected_monitor;
+
+ if (target_mon != selmon) {
+ if (selmon) |sel| {
+ unfocus_client(display, sel.sel, true);
+ }
+ monitor_mod.selected_monitor = target_mon;
+ } else if (client == null) {
+ return;
+ } else if (selmon) |sel| {
+ if (client.? == sel.sel) {
+ return;
+ }
+ }
+
+ focus(display, client);
+}
+
+fn handle_focus_in(display: *Display, event: *xlib.XFocusChangeEvent) void {
+ const selmon = monitor_mod.selected_monitor orelse return;
+ const selected = selmon.sel orelse return;
+ if (event.window != selected.window) {
+ set_focus(display, selected);
+ }
+}
+
+fn set_focus(display: *Display, client: *Client) void {
+ if (!client.never_focus) {
+ _ = xlib.XSetInputFocus(display.handle, client.window, xlib.RevertToPointerRoot, xlib.CurrentTime);
+ _ = xlib.XChangeProperty(display.handle, display.root, net_active_window, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&client.window), 1);
+ }
+ _ = send_event(display, client, wm_take_focus);
+}
+
+var last_motion_monitor: ?*Monitor = null;
+
+fn handle_motion_notify(display: *Display, event: *xlib.XMotionEvent) void {
+ if (event.window != display.root) {
+ return;
+ }
+
+ const target_mon = monitor_mod.rect_to_monitor(event.x_root, event.y_root, 1, 1);
+ if (target_mon != last_motion_monitor and last_motion_monitor != null) {
+ if (monitor_mod.selected_monitor) |selmon| {
+ unfocus_client(display, selmon.sel, true);
+ }
+ monitor_mod.selected_monitor = target_mon;
+ focus(display, null);
+ }
+ last_motion_monitor = target_mon;
+}
+
+fn handle_property_notify(display: *Display, event: *xlib.XPropertyEvent) void {
+ if (event.state == xlib.PropertyDelete) {
+ return;
+ }
+
+ const client = client_mod.window_to_client(event.window) orelse return;
+
+ if (event.atom == xlib.XA_WM_TRANSIENT_FOR) {
+ var trans: xlib.Window = 0;
+ if (!client.is_floating and xlib.XGetTransientForHint(display.handle, client.window, &trans) != 0) {
+ client.is_floating = client_mod.window_to_client(trans) != null;
+ if (client.is_floating) {
+ if (client.monitor) |monitor| {
+ arrange(monitor);
+ }
+ }
+ }
+ } else if (event.atom == xlib.XA_WM_NORMAL_HINTS) {
+ client.hints_valid = false;
+ } else if (event.atom == xlib.XA_WM_HINTS) {
+ update_wm_hints(display, client);
+ bar_mod.invalidate_bars();
+ } else if (event.atom == xlib.XA_WM_NAME or event.atom == net_wm_name) {
+ update_title(display, client);
+ } else if (event.atom == net_wm_window_type) {
+ update_window_type(display, client);
+ }
+}
+
+fn unfocus_client(display: *Display, client: ?*Client, reset_input_focus: bool) void {
+ const unfocus_target = client orelse return;
+ grabbuttons(display, unfocus_target, false);
+ _ = xlib.XSetWindowBorder(display.handle, unfocus_target.window, border_color_unfocused);
+ if (reset_input_focus) {
+ _ = xlib.XSetInputFocus(display.handle, display.root, xlib.RevertToPointerRoot, xlib.CurrentTime);
+ _ = xlib.XDeleteProperty(display.handle, display.root, net_active_window);
+ }
+}
+
+fn set_client_state(display: *Display, client: *Client, state: c_long) void {
+ var data: [2]c_long = .{ state, xlib.None };
+ _ = xlib.c.XChangeProperty(display.handle, client.window, wm_state, wm_state, 32, xlib.PropModeReplace, @ptrCast(&data), 2);
+}
+
+fn update_numlock_mask(display: *Display) void {
+ numlock_mask = 0;
+ const modmap = xlib.XGetModifierMapping(display.handle);
+ if (modmap == null) return;
+ defer _ = xlib.XFreeModifiermap(modmap);
+
+ const numlock_keycode = xlib.XKeysymToKeycode(display.handle, xlib.XK_Num_Lock);
+
+ var modifier_index: usize = 0;
+ while (modifier_index < 8) : (modifier_index += 1) {
+ var key_index: usize = 0;
+ while (key_index < @as(usize, @intCast(modmap.*.max_keypermod))) : (key_index += 1) {
+ const keycode = modmap.*.modifiermap[modifier_index * @as(usize, @intCast(modmap.*.max_keypermod)) + key_index];
+ if (keycode == numlock_keycode) {
+ numlock_mask = @as(c_uint, 1) << @intCast(modifier_index);
+ }
+ }
+ }
+}
+
+fn grabbuttons(display: *Display, client: *Client, focused: bool) void {
+ update_numlock_mask(display);
+ const modifiers = [_]c_uint{ 0, xlib.LockMask, numlock_mask, numlock_mask | xlib.LockMask };
+
+ _ = xlib.XUngrabButton(display.handle, xlib.AnyButton, xlib.AnyModifier, client.window);
+ if (!focused) {
+ _ = xlib.XGrabButton(
+ display.handle,
+ xlib.AnyButton,
+ xlib.AnyModifier,
+ client.window,
+ xlib.False,
+ xlib.ButtonPressMask | xlib.ButtonReleaseMask,
+ xlib.GrabModeSync,
+ xlib.GrabModeSync,
+ xlib.None,
+ xlib.None,
+ );
+ }
+ for (config.buttons.items) |button| {
+ if (button.click == .client_win) {
+ for (modifiers) |modifier| {
+ _ = xlib.XGrabButton(
+ display.handle,
+ @intCast(button.button),
+ button.mod_mask | modifier,
+ client.window,
+ xlib.False,
+ xlib.ButtonPressMask | xlib.ButtonReleaseMask,
+ xlib.GrabModeAsync,
+ xlib.GrabModeSync,
+ xlib.None,
+ xlib.None,
+ );
+ }
+ }
+ }
+}
+
+fn focus(display: *Display, target_client: ?*Client) void {
+ const selmon = monitor_mod.selected_monitor orelse return;
+
+ var focus_client = target_client;
+ if (focus_client == null or !client_mod.is_visible(focus_client.?)) {
+ focus_client = selmon.stack;
+ while (focus_client) |iter| {
+ if (client_mod.is_visible(iter)) break;
+ focus_client = iter.stack_next;
+ }
+ }
+
+ if (selmon.sel != null and selmon.sel != focus_client) {
+ unfocus_client(display, selmon.sel, false);
+ }
+
+ if (focus_client) |client| {
+ if (client.monitor != selmon) {
+ monitor_mod.selected_monitor = client.monitor;
+ }
+ if (client.is_urgent) {
+ set_urgent(display, client, false);
+ }
+ client_mod.detach_stack(client);
+ client_mod.attach_stack(client);
+ grabbuttons(display, client, true);
+ _ = xlib.XSetWindowBorder(display.handle, client.window, border_color_focused);
+ if (!client.never_focus) {
+ _ = xlib.XSetInputFocus(display.handle, client.window, xlib.RevertToPointerRoot, xlib.CurrentTime);
+ _ = xlib.XChangeProperty(display.handle, display.root, net_active_window, xlib.XA_WINDOW, 32, xlib.PropModeReplace, @ptrCast(&client.window), 1);
+ }
+ _ = send_event(display, client, wm_take_focus);
+ } else {
+ _ = xlib.XSetInputFocus(display.handle, display.root, xlib.RevertToPointerRoot, xlib.CurrentTime);
+ _ = xlib.XDeleteProperty(display.handle, display.root, net_active_window);
+ }
+
+ const current_selmon = monitor_mod.selected_monitor orelse return;
+ current_selmon.sel = focus_client;
+
+ if (focus_client) |client| {
+ if (is_scrolling_layout(current_selmon)) {
+ scroll_to_window(client, true);
+ }
+ }
+
+ bar_mod.invalidate_bars();
+}
+
+fn restack(display: *Display, monitor: *Monitor) void {
+ bar_mod.invalidate_bars();
+ const selected_client = monitor.sel orelse return;
+
+ if (selected_client.is_floating or monitor.lt[monitor.sel_lt] == null) {
+ _ = xlib.XRaiseWindow(display.handle, selected_client.window);
+ }
+
+ if (monitor.lt[monitor.sel_lt] != null) {
+ var window_changes: xlib.c.XWindowChanges = undefined;
+ window_changes.stack_mode = xlib.c.Below;
+ window_changes.sibling = monitor.bar_win;
+
+ var current = monitor.stack;
+ while (current) |client| {
+ if (!client.is_floating and client_mod.is_visible(client)) {
+ _ = xlib.c.XConfigureWindow(display.handle, client.window, xlib.c.CWSibling | xlib.c.CWStackMode, &window_changes);
+ window_changes.sibling = client.window;
+ }
+ current = client.stack_next;
+ }
+ }
+
+ _ = xlib.XSync(display.handle, xlib.False);
+
+ var discard_event: xlib.XEvent = undefined;
+ while (xlib.c.XCheckMaskEvent(display.handle, xlib.EnterWindowMask, &discard_event) != 0) {}
+}
+
+fn arrange(monitor: *Monitor) void {
+ if (display_global) |display| {
+ showhide(display, monitor);
+ }
+ if (monitor.lt[monitor.sel_lt]) |layout| {
+ if (layout.arrange_fn) |arrange_fn| {
+ arrange_fn(monitor);
+ }
+ }
+ if (display_global) |display| {
+ restack(display, monitor);
+ }
+}
+
+fn tick_animations() void {
+ if (!scroll_animation.is_active()) return;
+
+ const monitor = monitor_mod.selected_monitor orelse return;
+ if (scroll_animation.update()) |new_offset| {
+ monitor.scroll_offset = new_offset;
+ arrange(monitor);
+ }
+}
+
+fn is_scrolling_layout(monitor: *Monitor) bool {
+ if (monitor.lt[monitor.sel_lt]) |layout| {
+ return layout.arrange_fn == scrolling.layout.arrange_fn;
+ }
+ return false;
+}
+
+fn scroll_layout(direction: i32) void {
+ const monitor = monitor_mod.selected_monitor orelse return;
+ if (!is_scrolling_layout(monitor)) return;
+
+ const scroll_step = scrolling.get_scroll_step(monitor);
+ const max_scroll = scrolling.get_max_scroll(monitor);
+
+ const current = if (scroll_animation.is_active())
+ scroll_animation.target()
+ else
+ monitor.scroll_offset;
+
+ var target = current + direction * scroll_step;
+ target = @max(0, @min(target, max_scroll));
+
+ scroll_animation.start(monitor.scroll_offset, target, animation_config);
+}
+
+fn scroll_to_window(client: *Client, animate: bool) void {
+ const monitor = client.monitor orelse return;
+ if (!is_scrolling_layout(monitor)) return;
+
+ const target = scrolling.get_target_scroll_for_window(monitor, client);
+
+ if (animate) {
+ scroll_animation.start(monitor.scroll_offset, target, animation_config);
+ } else {
+ monitor.scroll_offset = target;
+ arrange(monitor);
+ }
+}
+
+fn showhide_client(display: *Display, client: ?*Client) void {
+ const target = client orelse return;
+ if (client_mod.is_visible(target)) {
+ _ = xlib.XMoveWindow(display.handle, target.window, target.x, target.y);
+ const monitor = target.monitor orelse return;
+ if ((monitor.lt[monitor.sel_lt] == null or target.is_floating) and !target.is_fullscreen) {
+ tiling.resize(target, target.x, target.y, target.width, target.height, false);
+ }
+ showhide_client(display, target.stack_next);
+ } else {
+ showhide_client(display, target.stack_next);
+ const client_width = target.width + 2 * target.border_width;
+ _ = xlib.XMoveWindow(display.handle, target.window, -2 * client_width, target.y);
+ }
+}
+
+fn showhide(display: *Display, monitor: *Monitor) void {
+ showhide_client(display, monitor.stack);
+}
+
+fn update_size_hints(display: *Display, client: *Client) void {
+ var size_hints: xlib.XSizeHints = undefined;
+ var msize: c_long = 0;
+
+ if (xlib.XGetWMNormalHints(display.handle, client.window, &size_hints, &msize) == 0) {
+ size_hints.flags = xlib.PSize;
+ }
+
+ if ((size_hints.flags & xlib.PBaseSize) != 0) {
+ client.base_width = size_hints.base_width;
+ client.base_height = size_hints.base_height;
+ } else if ((size_hints.flags & xlib.PMinSize) != 0) {
+ client.base_width = size_hints.min_width;
+ client.base_height = size_hints.min_height;
+ } else {
+ client.base_width = 0;
+ client.base_height = 0;
+ }
+
+ if ((size_hints.flags & xlib.PResizeInc) != 0) {
+ client.increment_width = size_hints.width_inc;
+ client.increment_height = size_hints.height_inc;
+ } else {
+ client.increment_width = 0;
+ client.increment_height = 0;
+ }
+
+ if ((size_hints.flags & xlib.PMaxSize) != 0) {
+ client.max_width = size_hints.max_width;
+ client.max_height = size_hints.max_height;
+ } else {
+ client.max_width = 0;
+ client.max_height = 0;
+ }
+
+ if ((size_hints.flags & xlib.PMinSize) != 0) {
+ client.min_width = size_hints.min_width;
+ client.min_height = size_hints.min_height;
+ } else if ((size_hints.flags & xlib.PBaseSize) != 0) {
+ client.min_width = size_hints.base_width;
+ client.min_height = size_hints.base_height;
+ } else {
+ client.min_width = 0;
+ client.min_height = 0;
+ }
+
+ if ((size_hints.flags & xlib.PAspect) != 0) {
+ client.min_aspect = @as(f32, @floatFromInt(size_hints.min_aspect.y)) / @as(f32, @floatFromInt(size_hints.min_aspect.x));
+ client.max_aspect = @as(f32, @floatFromInt(size_hints.max_aspect.x)) / @as(f32, @floatFromInt(size_hints.max_aspect.y));
+ } else {
+ client.min_aspect = 0.0;
+ client.max_aspect = 0.0;
+ }
+
+ client.is_fixed = (client.max_width != 0 and client.max_height != 0 and client.max_width == client.min_width and client.max_height == client.min_height);
+ client.hints_valid = true;
+}
+
+fn update_wm_hints(display: *Display, client: *Client) void {
+ const wmh = xlib.XGetWMHints(display.handle, client.window);
+ if (wmh) |hints| {
+ defer _ = xlib.XFree(@ptrCast(hints));
+
+ if (client == (monitor_mod.selected_monitor orelse return).sel and (hints.*.flags & xlib.XUrgencyHint) != 0) {
+ hints.*.flags = hints.*.flags & ~@as(c_long, xlib.XUrgencyHint);
+ _ = xlib.XSetWMHints(display.handle, client.window, hints);
+ } else {
+ client.is_urgent = (hints.*.flags & xlib.XUrgencyHint) != 0;
+ }
+
+ if ((hints.*.flags & xlib.InputHint) != 0) {
+ client.never_focus = hints.*.input == 0;
+ } else {
+ client.never_focus = false;
+ }
+ }
+}
+
+fn update_window_type(display: *Display, client: *Client) void {
+ const state = get_atom_prop(display, client, net_wm_state);
+ const window_type = get_atom_prop(display, client, net_wm_window_type);
+
+ if (state == net_wm_state_fullscreen) {
+ set_fullscreen(display, client, true);
+ }
+ if (window_type == net_wm_window_type_dialog) {
+ client.is_floating = true;
+ }
+}
+
+fn update_title(display: *Display, client: *Client) void {
+ if (!get_text_prop(display, client.window, net_wm_name, &client.name)) {
+ _ = get_text_prop(display, client.window, xlib.XA_WM_NAME, &client.name);
+ }
+ if (client.name[0] == 0) {
+ @memcpy(client.name[0..6], "broken");
+ }
+}
+
+fn get_atom_prop(display: *Display, client: *Client, prop: xlib.Atom) xlib.Atom {
+ var actual_type: xlib.Atom = undefined;
+ var actual_format: c_int = undefined;
+ var num_items: c_ulong = undefined;
+ var bytes_after: c_ulong = undefined;
+ var prop_data: [*c]u8 = undefined;
+
+ if (xlib.XGetWindowProperty(display.handle, client.window, prop, 0, @sizeOf(xlib.Atom), xlib.False, xlib.XA_ATOM, &actual_type, &actual_format, &num_items, &bytes_after, &prop_data) == 0 and prop_data != null) {
+ const atom: xlib.Atom = @as(*xlib.Atom, @ptrCast(@alignCast(prop_data))).*;
+ _ = xlib.XFree(@ptrCast(prop_data));
+ return atom;
+ }
+ return 0;
+}
+
+fn get_text_prop(display: *Display, window: xlib.Window, atom: xlib.Atom, text: *[256]u8) bool {
+ var name: xlib.XTextProperty = undefined;
+ text[0] = 0;
+
+ if (xlib.XGetTextProperty(display.handle, window, &name, atom) == 0 or name.nitems == 0) {
+ return false;
+ }
+
+ if (name.encoding == xlib.XA_STRING) {
+ const len = @min(name.nitems, 255);
+ @memcpy(text[0..len], name.value[0..len]);
+ text[len] = 0;
+ } else {
+ var list: [*c][*c]u8 = undefined;
+ var count: c_int = undefined;
+ if (xlib.XmbTextPropertyToTextList(display.handle, &name, &list, &count) >= xlib.Success and count > 0 and list[0] != null) {
+ const str = std.mem.sliceTo(list[0], 0);
+ const copy_len = @min(str.len, 255);
+ @memcpy(text[0..copy_len], str[0..copy_len]);
+ text[copy_len] = 0;
+ xlib.XFreeStringList(list);
+ }
+ }
+ text[255] = 0;
+ _ = xlib.XFree(@ptrCast(name.value));
+ return true;
+}
+
+fn apply_rules(display: *Display, client: *Client) void {
+ var class_hint: xlib.XClassHint = .{ .res_name = null, .res_class = null };
+ _ = xlib.XGetClassHint(display.handle, client.window, &class_hint);
+
+ const class_str: []const u8 = if (class_hint.res_class) |ptr| std.mem.sliceTo(ptr, 0) else "";
+ const instance_str: []const u8 = if (class_hint.res_name) |ptr| std.mem.sliceTo(ptr, 0) else "";
+
+ client.is_floating = false;
+ client.tags = 0;
+
+ for (config.rules.items) |rule| {
+ const class_matches = if (rule.class) |rc| std.mem.indexOf(u8, class_str, rc) != null else true;
+ const instance_matches = if (rule.instance) |ri| std.mem.indexOf(u8, instance_str, ri) != null else true;
+ const title_matches = if (rule.title) |rt| std.mem.indexOf(u8, std.mem.sliceTo(&client.name, 0), rt) != null else true;
+
+ if (class_matches and instance_matches and title_matches) {
+ client.is_floating = rule.is_floating;
+ client.tags |= rule.tags;
+ if (rule.monitor >= 0) {
+ var target = monitor_mod.monitors;
+ var index: i32 = 0;
+ while (target) |mon| {
+ if (index == rule.monitor) {
+ client.monitor = mon;
+ break;
+ }
+ index += 1;
+ target = mon.next;
+ }
+ }
+ }
+ }
+
+ if (class_hint.res_class) |ptr| {
+ _ = xlib.XFree(@ptrCast(ptr));
+ }
+ if (class_hint.res_name) |ptr| {
+ _ = xlib.XFree(@ptrCast(ptr));
+ }
+
+ const monitor = client.monitor orelse return;
+ if (client.tags == 0) {
+ client.tags = monitor.tagset[monitor.sel_tags];
+ }
+}
+
+fn update_client_list(display: *Display) void {
+ _ = xlib.XDeleteProperty(display.handle, display.root, net_client_list);
+
+ var current_monitor = monitor_mod.monitors;
+ while (current_monitor) |monitor| {
+ var current_client = monitor.clients;
+ while (current_client) |client| {
+ _ = xlib.XChangeProperty(display.handle, display.root, net_client_list, xlib.XA_WINDOW, 32, xlib.PropModeAppend, @ptrCast(&client.window), 1);
+ current_client = client.next;
+ }
+ current_monitor = monitor.next;
+ }
+}
+
+fn send_event(display: *Display, client: *Client, protocol: xlib.Atom) bool {
+ var protocols: [*c]xlib.Atom = undefined;
+ var num_protocols: c_int = 0;
+ var exists = false;
+
+ if (xlib.XGetWMProtocols(display.handle, client.window, &protocols, &num_protocols) != 0) {
+ var index: usize = 0;
+ while (index < @as(usize, @intCast(num_protocols))) : (index += 1) {
+ if (protocols[index] == protocol) {
+ exists = true;
+ break;
+ }
+ }
+ _ = xlib.XFree(@ptrCast(protocols));
+ }
+
+ if (exists) {
+ var event: xlib.XEvent = undefined;
+ event.type = xlib.ClientMessage;
+ event.xclient.window = client.window;
+ event.xclient.message_type = wm_protocols;
+ event.xclient.format = 32;
+ event.xclient.data.l[0] = @intCast(protocol);
+ event.xclient.data.l[1] = xlib.CurrentTime;
+ _ = xlib.XSendEvent(display.handle, client.window, xlib.False, xlib.NoEventMask, &event);
+ }
+ return exists;
+}
+
+fn set_urgent(display: *Display, client: *Client, urgent: bool) void {
+ client.is_urgent = urgent;
+ const wmh = xlib.XGetWMHints(display.handle, client.window);
+ if (wmh) |hints| {
+ if (urgent) {
+ hints.*.flags = hints.*.flags | xlib.XUrgencyHint;
+ } else {
+ hints.*.flags = hints.*.flags & ~@as(c_long, xlib.XUrgencyHint);
+ }
+ _ = xlib.XSetWMHints(display.handle, client.window, hints);
+ _ = xlib.XFree(@ptrCast(hints));
+ }
+}
diff --git a/src/monitor.rs b/src/monitor.rs
deleted file mode 100644
index caa18ad..0000000
--- a/src/monitor.rs
+++ /dev/null
@@ -1,198 +0,0 @@
-use crate::client::TagMask;
-use crate::errors::WmError;
-use x11rb::protocol::xinerama::ConnectionExt as _;
-use x11rb::protocol::xproto::{Screen, Window};
-use x11rb::rust_connection::RustConnection;
-
-type WmResult<T> = Result<T, WmError>;
-
-#[derive(Debug, Clone)]
-pub struct Pertag {
- pub current_tag: usize,
- pub previous_tag: usize,
- pub num_masters: Vec<i32>,
- pub master_factors: Vec<f32>,
- pub layouts: Vec<String>,
- pub show_bars: Vec<bool>,
-}
-
-impl Pertag {
- pub fn new(
- num_tags: usize,
- default_num_master: i32,
- default_master_factor: f32,
- default_show_bar: bool,
- default_layout: &str,
- ) -> Self {
- let len = num_tags + 1;
- Self {
- current_tag: 1,
- previous_tag: 1,
- num_masters: vec![default_num_master; len],
- master_factors: vec![default_master_factor; len],
- layouts: vec![default_layout.to_string(); len],
- show_bars: vec![default_show_bar; len],
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Monitor {
- pub layout_symbol: String,
- pub master_factor: f32,
- pub num_master: i32,
- pub monitor_number: usize,
- pub bar_y_position: i32,
- pub screen_x: i32,
- pub screen_y: i32,
- pub screen_width: i32,
- pub screen_height: i32,
- pub window_area_x: i32,
- pub window_area_y: i32,
- pub window_area_width: i32,
- pub window_area_height: i32,
- pub gap_inner_horizontal: i32,
- pub gap_inner_vertical: i32,
- pub gap_outer_horizontal: i32,
- pub gap_outer_vertical: i32,
- pub selected_tags_index: usize,
- pub selected_layout_index: usize,
- pub tagset: [u32; 2],
- pub show_bar: bool,
- pub top_bar: bool,
- pub clients_head: Option<Window>,
- pub selected_client: Option<Window>,
- pub stack_head: Option<Window>,
- pub bar_window: Option<Window>,
- pub layout_indices: [usize; 2],
- pub scroll_offset: i32,
- pub pertag: Option<Pertag>,
-}
-
-impl Monitor {
- pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
- Self {
- layout_symbol: String::from("[]"),
- master_factor: 0.55,
- num_master: 1,
- monitor_number: 0,
- bar_y_position: 0,
- screen_x: x,
- screen_y: y,
- screen_width: width as i32,
- screen_height: height as i32,
- window_area_x: x,
- window_area_y: y,
- window_area_width: width as i32,
- window_area_height: height as i32,
- gap_inner_horizontal: 3,
- gap_inner_vertical: 3,
- gap_outer_horizontal: 3,
- gap_outer_vertical: 3,
- selected_tags_index: 0,
- selected_layout_index: 0,
- tagset: [1, 1],
- show_bar: true,
- top_bar: true,
- clients_head: None,
- selected_client: None,
- stack_head: None,
- bar_window: None,
- layout_indices: [0, 1],
- scroll_offset: 0,
- pertag: None,
- }
- }
-
- pub fn init_pertag(&mut self, num_tags: usize, default_layout: &str) {
- self.pertag = Some(Pertag::new(
- num_tags,
- self.num_master,
- self.master_factor,
- self.show_bar,
- default_layout,
- ));
- }
-
- pub fn contains_point(&self, x: i32, y: i32) -> bool {
- x >= self.screen_x
- && x < self.screen_x + self.screen_width
- && y >= self.screen_y
- && y < self.screen_y + self.screen_height
- }
-
- pub fn get_selected_tag(&self) -> TagMask {
- self.tagset[self.selected_tags_index]
- }
-}
-
-pub fn detect_monitors(
- connection: &RustConnection,
- screen: &Screen,
- _root: Window,
-) -> WmResult<Vec<Monitor>> {
- let fallback_monitors = || {
- vec![Monitor::new(
- 0,
- 0,
- screen.width_in_pixels as u32,
- screen.height_in_pixels as u32,
- )]
- };
-
- let mut monitors = Vec::<Monitor>::new();
-
- let xinerama_active = connection
- .xinerama_is_active()
- .ok()
- .and_then(|cookie| cookie.reply().ok())
- .is_some_and(|reply| reply.state != 0);
-
- if xinerama_active {
- let Ok(xinerama_cookie) = connection.xinerama_query_screens() else {
- return Ok(fallback_monitors());
- };
- let Ok(xinerama_reply) = xinerama_cookie.reply() else {
- return Ok(fallback_monitors());
- };
-
- for screen_info in &xinerama_reply.screen_info {
- let has_valid_dimensions = screen_info.width > 0 && screen_info.height > 0;
- if !has_valid_dimensions {
- continue;
- }
-
- let x_position = screen_info.x_org as i32;
- let y_position = screen_info.y_org as i32;
- let width_in_pixels = screen_info.width as u32;
- let height_in_pixels = screen_info.height as u32;
-
- let is_duplicate_monitor = monitors.iter().any(|monitor| {
- monitor.screen_x == x_position
- && monitor.screen_y == y_position
- && monitor.screen_width == width_in_pixels as i32
- && monitor.screen_height == height_in_pixels as i32
- });
-
- if !is_duplicate_monitor {
- monitors.push(Monitor::new(
- x_position,
- y_position,
- width_in_pixels,
- height_in_pixels,
- ));
- }
- }
- }
-
- if monitors.is_empty() {
- monitors = fallback_monitors();
- }
-
- monitors.sort_by(|a, b| match a.screen_y.cmp(&b.screen_y) {
- std::cmp::Ordering::Equal => a.screen_x.cmp(&b.screen_x),
- other => other,
- });
-
- Ok(monitors)
-}
diff --git a/src/monitor.zig b/src/monitor.zig
new file mode 100644
index 0000000..495ee91
--- /dev/null
+++ b/src/monitor.zig
@@ -0,0 +1,154 @@
+const std = @import("std");
+const xlib = @import("x11/xlib.zig");
+const Client = @import("client.zig").Client;
+
+pub const Layout = struct {
+ symbol: []const u8,
+ arrange_fn: ?*const fn (*Monitor) void,
+};
+
+pub const Pertag = struct {
+ curtag: u32 = 1,
+ prevtag: u32 = 1,
+ nmasters: [10]i32 = [_]i32{1} ** 10,
+ mfacts: [10]f32 = [_]f32{0.55} ** 10,
+ sellts: [10]u32 = [_]u32{0} ** 10,
+ ltidxs: [10][4]?*const Layout = [_][4]?*const Layout{.{ null, null, null, null }} ** 10,
+ showbars: [10]bool = [_]bool{true} ** 10,
+};
+
+pub const Monitor = struct {
+ lt_symbol: [16]u8 = std.mem.zeroes([16]u8),
+ mfact: f32 = 0.55,
+ nmaster: i32 = 1,
+ num: i32 = 0,
+ bar_y: i32 = 0,
+ mon_x: i32 = 0,
+ mon_y: i32 = 0,
+ mon_w: i32 = 0,
+ mon_h: i32 = 0,
+ win_x: i32 = 0,
+ win_y: i32 = 0,
+ win_w: i32 = 0,
+ win_h: i32 = 0,
+ gap_inner_h: i32 = 0,
+ gap_inner_v: i32 = 0,
+ gap_outer_h: i32 = 0,
+ gap_outer_v: i32 = 0,
+ scroll_offset: i32 = 0,
+ sel_tags: u32 = 0,
+ sel_lt: u32 = 0,
+ tagset: [2]u32 = .{ 1, 1 },
+ show_bar: bool = true,
+ top_bar: bool = true,
+ clients: ?*Client = null,
+ sel: ?*Client = null,
+ stack: ?*Client = null,
+ next: ?*Monitor = null,
+ bar_win: xlib.Window = 0,
+ lt: [4]?*const Layout = .{ null, null, null, null },
+ pertag: Pertag = Pertag{},
+};
+
+pub var monitors: ?*Monitor = null;
+pub var selected_monitor: ?*Monitor = null;
+
+var allocator: std.mem.Allocator = undefined;
+
+pub fn init(alloc: std.mem.Allocator) void {
+ allocator = alloc;
+}
+
+pub fn create() ?*Monitor {
+ const mon = allocator.create(Monitor) catch return null;
+ mon.* = Monitor{};
+ return mon;
+}
+
+pub fn destroy(mon: *Monitor) void {
+ allocator.destroy(mon);
+}
+
+var root_window: xlib.Window = 0;
+var display_handle: ?*xlib.Display = null;
+
+pub fn set_root_window(root: xlib.Window, display: *xlib.Display) void {
+ root_window = root;
+ display_handle = display;
+}
+
+pub fn window_to_monitor(win: xlib.Window) ?*Monitor {
+ if (win == root_window and display_handle != null) {
+ var root_x: c_int = undefined;
+ var root_y: c_int = undefined;
+ var dummy_win: xlib.Window = undefined;
+ var dummy_int: c_int = undefined;
+ var dummy_uint: c_uint = undefined;
+ if (xlib.XQueryPointer(display_handle.?, root_window, &dummy_win, &dummy_win, &root_x, &root_y, &dummy_int, &dummy_int, &dummy_uint) != 0) {
+ return rect_to_monitor(root_x, root_y, 1, 1);
+ }
+ }
+
+ var current = monitors;
+ while (current) |monitor| {
+ if (monitor.bar_win == win) {
+ return monitor;
+ }
+ current = monitor.next;
+ }
+
+ const client = @import("client.zig").window_to_client(win);
+ if (client) |found_client| {
+ return found_client.monitor;
+ }
+
+ return selected_monitor;
+}
+
+pub fn rect_to_monitor(x: i32, y: i32, width: i32, height: i32) ?*Monitor {
+ var result = selected_monitor;
+ var max_area: i32 = 0;
+
+ var current = monitors;
+ while (current) |monitor| {
+ const intersect_x = @max(0, @min(x + width, monitor.win_x + monitor.win_w) - @max(x, monitor.win_x));
+ const intersect_y = @max(0, @min(y + height, monitor.win_y + monitor.win_h) - @max(y, monitor.win_y));
+ const area = intersect_x * intersect_y;
+ if (area > max_area) {
+ max_area = area;
+ result = monitor;
+ }
+ current = monitor.next;
+ }
+ return result;
+}
+
+pub fn dir_to_monitor(direction: i32) ?*Monitor {
+ var target: ?*Monitor = null;
+
+ if (direction > 0) {
+ target = if (selected_monitor) |current| current.next else null;
+ if (target == null) {
+ target = monitors;
+ }
+ } else if (selected_monitor == monitors) {
+ var last = monitors;
+ while (last) |iter| {
+ if (iter.next == null) {
+ target = iter;
+ break;
+ }
+ last = iter.next;
+ }
+ } else {
+ var previous = monitors;
+ while (previous) |iter| {
+ if (iter.next == selected_monitor) {
+ target = iter;
+ break;
+ }
+ previous = iter.next;
+ }
+ }
+ return target;
+}
diff --git a/src/overlay/error.rs b/src/overlay/error.rs
deleted file mode 100644
index 128729e..0000000
--- a/src/overlay/error.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-use super::{Overlay, OverlayBase};
-use crate::bar::font::Font;
-use crate::errors::{ConfigError, X11Error};
-use x11rb::connection::Connection;
-use x11rb::protocol::xproto::*;
-use x11rb::rust_connection::RustConnection;
-
-const PADDING: i16 = 20;
-const LINE_SPACING: i16 = 5;
-const BORDER_WIDTH: u16 = 2;
-const BORDER_COLOR: u32 = 0xff5555;
-
-pub struct ErrorOverlay {
- base: OverlayBase,
- lines: Vec<String>,
-}
-
-impl ErrorOverlay {
- pub fn new(
- connection: &RustConnection,
- screen: &Screen,
- screen_num: usize,
- display: *mut x11::xlib::Display,
- _font: &Font,
- _max_width: u16,
- ) -> Result<Self, X11Error> {
- let base = OverlayBase::new(
- connection,
- screen,
- screen_num,
- display,
- 400,
- 200,
- BORDER_WIDTH,
- BORDER_COLOR,
- 0x1a1a1a,
- 0xffffff,
- )?;
-
- Ok(ErrorOverlay {
- base,
- lines: Vec::new(),
- })
- }
-
- pub fn show_error(
- &mut self,
- connection: &RustConnection,
- font: &Font,
- error: ConfigError,
- monitor_x: i16,
- monitor_y: i16,
- screen_width: u16,
- screen_height: u16,
- ) -> Result<(), X11Error> {
- let max_line_width = (screen_width as i16 / 2 - PADDING * 4).max(300) as u16;
- let error_with_instruction = format!("{}\n\nFix the config file and reload.", error);
- self.lines = self.wrap_text(&error_with_instruction, font, max_line_width);
-
- let mut content_width = 0u16;
- for line in &self.lines {
- let line_width = font.text_width(line);
- if line_width > content_width {
- content_width = line_width;
- }
- }
-
- let width = content_width + (PADDING as u16 * 2);
- let line_height = font.height() + LINE_SPACING as u16;
- let height = (self.lines.len() as u16 * line_height) + (PADDING as u16 * 2);
-
- let x = monitor_x + ((screen_width - width) / 2) as i16;
- let y = monitor_y + ((screen_height - height) / 2) as i16;
-
- self.base.configure(connection, x, y, width, height)?;
- self.base.is_visible = true;
- self.draw(connection, font)?;
- self.base.show(connection)?;
- Ok(())
- }
-
- fn wrap_text(&self, text: &str, font: &Font, max_width: u16) -> Vec<String> {
- let mut lines = Vec::new();
- for paragraph in text.lines() {
- if paragraph.trim().is_empty() {
- lines.push(String::new());
- continue;
- }
-
- let words: Vec<&str> = paragraph.split_whitespace().collect();
- let mut current_line = String::new();
-
- for word in words {
- let test_line = if current_line.is_empty() {
- word.to_string()
- } else {
- format!("{} {}", current_line, word)
- };
- if font.text_width(&test_line) <= max_width {
- current_line = test_line;
- } else {
- if !current_line.is_empty() {
- lines.push(current_line);
- }
- current_line = word.to_string();
- }
- }
- if !current_line.is_empty() {
- lines.push(current_line);
- }
- }
- lines
- }
-}
-
-impl Overlay for ErrorOverlay {
- fn window(&self) -> Window {
- self.base.window
- }
-
- fn is_visible(&self) -> bool {
- self.base.is_visible
- }
-
- fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
- self.base.hide(connection)?;
- self.lines.clear();
- Ok(())
- }
-
- fn draw(&self, connection: &RustConnection, font: &Font) -> Result<(), X11Error> {
- if !self.base.is_visible {
- return Ok(());
- }
- self.base.draw_background(connection)?;
- let line_height = font.height() + LINE_SPACING as u16;
- let mut y = PADDING + font.ascent();
- for line in &self.lines {
- self.base
- .font_draw
- .draw_text(font, self.base.foreground_color, PADDING, y, line);
- y += line_height as i16;
- }
- connection.flush()?;
- self.base.font_draw.sync();
- Ok(())
- }
-}
diff --git a/src/overlay/keybind.rs b/src/overlay/keybind.rs
deleted file mode 100644
index 278bf28..0000000
--- a/src/overlay/keybind.rs
+++ /dev/null
@@ -1,320 +0,0 @@
-use super::{Overlay, OverlayBase};
-use crate::bar::font::Font;
-use crate::errors::X11Error;
-use crate::keyboard::KeyAction;
-use crate::keyboard::handlers::{KeyBinding, KeyPress};
-use std::time::Instant;
-use x11rb::connection::Connection;
-use x11rb::protocol::xproto::*;
-use x11rb::rust_connection::RustConnection;
-
-const PADDING: i16 = 24;
-const KEY_ACTION_SPACING: i16 = 20;
-const LINE_SPACING: i16 = 8;
-const BORDER_WIDTH: u16 = 4;
-const BORDER_COLOR: u32 = 0x7fccff;
-const TITLE_BOTTOM_MARGIN: i16 = 20;
-const INPUT_SUPPRESS_MS: u128 = 200;
-
-pub struct KeybindOverlay {
- base: OverlayBase,
- keybindings: Vec<(String, String)>,
- key_bg_color: u32,
- modkey: KeyButMask,
- last_shown_at: Option<Instant>,
- max_key_width: u16,
-}
-
-impl KeybindOverlay {
- pub fn new(
- connection: &RustConnection,
- screen: &Screen,
- screen_num: usize,
- display: *mut x11::xlib::Display,
- modkey: KeyButMask,
- ) -> Result<Self, X11Error> {
- let base = OverlayBase::new(
- connection,
- screen,
- screen_num,
- display,
- 800,
- 600,
- BORDER_WIDTH,
- BORDER_COLOR,
- 0x1a1a1a,
- 0xffffff,
- )?;
-
- Ok(KeybindOverlay {
- base,
- keybindings: Vec::new(),
- key_bg_color: 0x2a2a2a,
- modkey,
- last_shown_at: None,
- max_key_width: 0,
- })
- }
-
- pub fn show(
- &mut self,
- connection: &RustConnection,
- font: &Font,
- keybindings: &[KeyBinding],
- monitor_x: i16,
- monitor_y: i16,
- screen_width: u16,
- screen_height: u16,
- ) -> Result<(), X11Error> {
- self.keybindings = self.collect_keybindings(keybindings);
-
- let title = "Important Keybindings";
- let title_width = font.text_width(title);
-
- let mut max_key_width = 0u16;
- let mut max_action_width = 0u16;
-
- for (key, action) in &self.keybindings {
- let key_width = font.text_width(key);
- let action_width = font.text_width(action);
- if key_width > max_key_width {
- max_key_width = key_width;
- }
- if action_width > max_action_width {
- max_action_width = action_width;
- }
- }
-
- let content_width = max_key_width + KEY_ACTION_SPACING as u16 + max_action_width;
- let min_width = title_width.max(content_width);
-
- let width = min_width + (PADDING as u16 * 2);
-
- let line_height = font.height() + LINE_SPACING as u16;
- let title_height = font.height() + TITLE_BOTTOM_MARGIN as u16;
- let height =
- title_height + (self.keybindings.len() as u16 * line_height) + (PADDING as u16 * 2);
-
- let x = monitor_x + ((screen_width - width) / 2) as i16;
- let y = monitor_y + ((screen_height - height) / 2) as i16;
-
- self.base.configure(connection, x, y, width, height)?;
-
- self.last_shown_at = Some(Instant::now());
- self.max_key_width = max_key_width;
-
- self.base.is_visible = true;
- self.draw(connection, font)?;
-
- self.base.show(connection)?;
-
- Ok(())
- }
-
- pub fn toggle(
- &mut self,
- connection: &RustConnection,
- font: &Font,
- keybindings: &[KeyBinding],
- monitor_x: i16,
- monitor_y: i16,
- screen_width: u16,
- screen_height: u16,
- ) -> Result<(), X11Error> {
- if self.base.is_visible {
- self.hide(connection)?;
- } else {
- self.show(
- connection,
- font,
- keybindings,
- monitor_x,
- monitor_y,
- screen_width,
- screen_height,
- )?;
- }
- Ok(())
- }
-
- pub fn should_suppress_input(&self) -> bool {
- if let Some(shown_at) = self.last_shown_at {
- shown_at.elapsed().as_millis() < INPUT_SUPPRESS_MS
- } else {
- false
- }
- }
-
- fn collect_keybindings(&self, keybindings: &[KeyBinding]) -> Vec<(String, String)> {
- let mut result = Vec::new();
-
- let priority_actions = [
- KeyAction::ShowKeybindOverlay,
- KeyAction::Quit,
- KeyAction::Restart,
- KeyAction::KillClient,
- KeyAction::Spawn,
- KeyAction::SpawnTerminal,
- KeyAction::ToggleFullScreen,
- KeyAction::ToggleFloating,
- KeyAction::CycleLayout,
- KeyAction::FocusStack,
- KeyAction::ViewTag,
- ];
-
- for &action in &priority_actions {
- let binding = keybindings
- .iter()
- .filter(|kb| kb.func == action)
- .min_by_key(|kb| kb.keys.len());
-
- if let Some(binding) = binding
- && !binding.keys.is_empty()
- {
- let key_str = self.format_key_combo(&binding.keys[0]);
- let action_str = self.action_description(binding);
- result.push((key_str, action_str));
- }
- }
-
- result
- }
-
- fn format_key_combo(&self, key: &KeyPress) -> String {
- let mut parts = Vec::new();
-
- for modifier in &key.modifiers {
- let mod_str = match *modifier {
- m if m == self.modkey => "Mod",
- KeyButMask::SHIFT => "Shift",
- KeyButMask::CONTROL => "Ctrl",
- KeyButMask::MOD1 => "Alt",
- KeyButMask::MOD4 => "Super",
- _ => continue,
- };
- parts.push(mod_str.to_string());
- }
-
- parts.push(crate::keyboard::keysyms::format_keysym(key.keysym));
-
- parts.join(" + ")
- }
-
- fn action_description(&self, binding: &KeyBinding) -> String {
- use crate::keyboard::Arg;
-
- match binding.func {
- KeyAction::ShowKeybindOverlay => "Show This Keybind Help".to_string(),
- KeyAction::Quit => "Quit Window Manager".to_string(),
- KeyAction::Restart => "Restart Window Manager".to_string(),
- KeyAction::KillClient => "Close Focused Window".to_string(),
- KeyAction::Spawn => match &binding.arg {
- Arg::Str(cmd) => format!("Launch: {}", cmd),
- Arg::Array(arr) if !arr.is_empty() => format!("Launch: {}", arr[0]),
- _ => "Launch Program".to_string(),
- },
- KeyAction::SpawnTerminal => "Launch Terminal".to_string(),
- KeyAction::FocusStack => "Focus Next/Previous Window".to_string(),
- KeyAction::MoveStack => "Move Window Up/Down Stack".to_string(),
- KeyAction::ViewTag => match &binding.arg {
- Arg::Int(n) => format!("View Workspace {}", n),
- _ => "View Workspace".to_string(),
- },
- KeyAction::ViewNextTag => "View Next Workspace".to_string(),
- KeyAction::ViewPreviousTag => "View Previous Workspace".to_string(),
- KeyAction::ViewNextNonEmptyTag => "View Next Non-Empty Workspace".to_string(),
- KeyAction::ViewPreviousNonEmptyTag => "View Previous Non-Empty Workspace".to_string(),
- KeyAction::ToggleView => match &binding.arg {
- Arg::Int(n) => format!("Toggle View Workspace {}", n),
- _ => "Toggle View Workspace".to_string(),
- },
- KeyAction::MoveToTag => "Move Window to Workspace".to_string(),
- KeyAction::ToggleTag => "Toggle Window on Workspace".to_string(),
- KeyAction::ToggleGaps => "Toggle Window Gaps".to_string(),
- KeyAction::ToggleFullScreen => "Toggle Fullscreen Mode".to_string(),
- KeyAction::ToggleFloating => "Toggle Floating Mode".to_string(),
- KeyAction::ChangeLayout => "Change Layout".to_string(),
- KeyAction::CycleLayout => "Cycle Through Layouts".to_string(),
- KeyAction::FocusMonitor => "Focus Next Monitor".to_string(),
- KeyAction::TagMonitor => "Send Window to Monitor".to_string(),
- KeyAction::SetMasterFactor => "Adjust Master Area Size".to_string(),
- KeyAction::IncNumMaster => "Adjust Number of Master Windows".to_string(),
- KeyAction::ScrollLeft => "Scroll Layout Left".to_string(),
- KeyAction::ScrollRight => "Scroll Layout Right".to_string(),
- KeyAction::None => "No Action".to_string(),
- }
- }
-}
-
-impl Overlay for KeybindOverlay {
- fn window(&self) -> Window {
- self.base.window
- }
-
- fn is_visible(&self) -> bool {
- self.base.is_visible
- }
-
- fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
- self.base.hide(connection)?;
- self.last_shown_at = None;
- self.keybindings.clear();
- Ok(())
- }
-
- fn draw(&self, connection: &RustConnection, font: &Font) -> Result<(), X11Error> {
- if !self.base.is_visible {
- return Ok(());
- }
-
- self.base.draw_background(connection)?;
-
- let title = "Important Keybindings";
- let title_width = font.text_width(title);
- let title_x = ((self.base.width - title_width) / 2) as i16;
- let title_y = PADDING + font.ascent();
-
- self.base
- .font_draw
- .draw_text(font, self.base.foreground_color, title_x, title_y, title);
-
- let line_height = font.height() + LINE_SPACING as u16;
- let mut y = PADDING + font.height() as i16 + TITLE_BOTTOM_MARGIN + font.ascent();
-
- for (key, action) in &self.keybindings {
- let key_width = font.text_width(key);
- let key_x = PADDING;
-
- connection.change_gc(
- self.base.graphics_context,
- &ChangeGCAux::new().foreground(self.key_bg_color),
- )?;
- connection.poly_fill_rectangle(
- self.base.window,
- self.base.graphics_context,
- &[Rectangle {
- x: key_x - 4,
- y: y - font.ascent() - 2,
- width: key_width + 8,
- height: font.height() + 4,
- }],
- )?;
-
- self.base
- .font_draw
- .draw_text(font, self.base.foreground_color, key_x, y, key);
-
- let action_x = PADDING + self.max_key_width as i16 + KEY_ACTION_SPACING;
- self.base
- .font_draw
- .draw_text(font, self.base.foreground_color, action_x, y, action);
-
- y += line_height as i16;
- }
-
- connection.flush()?;
- self.base.font_draw.sync();
-
- Ok(())
- }
-}
diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs
deleted file mode 100644
index bd87f63..0000000
--- a/src/overlay/mod.rs
+++ /dev/null
@@ -1,156 +0,0 @@
-use crate::bar::font::{Font, FontDraw};
-use crate::errors::X11Error;
-use x11rb::COPY_DEPTH_FROM_PARENT;
-use x11rb::connection::Connection;
-use x11rb::protocol::xproto::*;
-use x11rb::rust_connection::RustConnection;
-
-pub mod error;
-pub mod keybind;
-
-pub use error::ErrorOverlay;
-pub use keybind::KeybindOverlay;
-
-pub trait Overlay {
- fn window(&self) -> Window;
- fn is_visible(&self) -> bool;
- fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error>;
- fn draw(&self, connection: &RustConnection, font: &Font) -> Result<(), X11Error>;
-}
-
-pub struct OverlayBase {
- pub window: Window,
- pub width: u16,
- pub height: u16,
- pub graphics_context: Gcontext,
- pub font_draw: FontDraw,
- pub is_visible: bool,
- pub background_color: u32,
- pub foreground_color: u32,
-}
-
-impl OverlayBase {
- pub fn new(
- connection: &RustConnection,
- screen: &Screen,
- screen_num: usize,
- display: *mut x11::xlib::Display,
- width: u16,
- height: u16,
- border_width: u16,
- border_color: u32,
- background_color: u32,
- foreground_color: u32,
- ) -> Result<Self, X11Error> {
- let window = connection.generate_id()?;
- let graphics_context = connection.generate_id()?;
-
- connection.create_window(
- COPY_DEPTH_FROM_PARENT,
- window,
- screen.root,
- 0,
- 0,
- width,
- height,
- border_width,
- WindowClass::INPUT_OUTPUT,
- screen.root_visual,
- &CreateWindowAux::new()
- .background_pixel(background_color)
- .border_pixel(border_color)
- .event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS | EventMask::KEY_PRESS)
- .override_redirect(1),
- )?;
-
- connection.create_gc(
- graphics_context,
- window,
- &CreateGCAux::new()
- .foreground(foreground_color)
- .background(background_color),
- )?;
-
- connection.flush()?;
-
- let visual = unsafe { x11::xlib::XDefaultVisual(display, screen_num as i32) };
- let colormap = unsafe { x11::xlib::XDefaultColormap(display, screen_num as i32) };
-
- let font_draw = FontDraw::new(display, window as x11::xlib::Drawable, visual, colormap)?;
-
- Ok(OverlayBase {
- window,
- width,
- height,
- graphics_context,
- font_draw,
- is_visible: false,
- background_color,
- foreground_color,
- })
- }
-
- pub fn configure(
- &mut self,
- connection: &RustConnection,
- x: i16,
- y: i16,
- width: u16,
- height: u16,
- ) -> Result<(), X11Error> {
- self.width = width;
- self.height = height;
-
- connection.configure_window(
- self.window,
- &ConfigureWindowAux::new()
- .x(x as i32)
- .y(y as i32)
- .width(width as u32)
- .height(height as u32),
- )?;
-
- Ok(())
- }
-
- pub fn show(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
- connection.configure_window(
- self.window,
- &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
- )?;
-
- connection.map_window(self.window)?;
- connection.flush()?;
-
- self.is_visible = true;
-
- Ok(())
- }
-
- pub fn hide(&mut self, connection: &RustConnection) -> Result<(), X11Error> {
- if self.is_visible {
- connection.unmap_window(self.window)?;
- connection.flush()?;
- self.is_visible = false;
- }
- Ok(())
- }
-
- pub fn draw_background(&self, connection: &RustConnection) -> Result<(), X11Error> {
- connection.change_gc(
- self.graphics_context,
- &ChangeGCAux::new().foreground(self.background_color),
- )?;
- connection.poly_fill_rectangle(
- self.window,
- self.graphics_context,
- &[Rectangle {
- x: 0,
- y: 0,
- width: self.width,
- height: self.height,
- }],
- )?;
- Ok(())
- }
-}
diff --git a/src/signal.rs b/src/signal.rs
deleted file mode 100644
index c5cec0b..0000000
--- a/src/signal.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use std::process::{Command, Stdio};
-
-pub fn spawn_detached(cmd: &str) {
- if let Ok(mut child) = Command::new("sh")
- .arg("-c")
- .arg(format!("({}) &", cmd))
- .stdin(Stdio::null())
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .spawn()
- {
- let _ = child.wait();
- }
-}
-
-pub fn spawn_detached_with_args(program: &str, args: &[&str]) {
- let escaped_args: Vec<String> = args.iter().map(|a| shell_escape(a)).collect();
- let full_cmd = if escaped_args.is_empty() {
- program.to_string()
- } else {
- format!("{} {}", program, escaped_args.join(" "))
- };
- spawn_detached(&full_cmd)
-}
-
-fn shell_escape(s: &str) -> String {
- if s.contains(|c: char| c.is_whitespace() || c == '\'' || c == '"' || c == '\\') {
- format!("'{}'", s.replace('\'', "'\\''"))
- } else {
- s.to_string()
- }
-}
diff --git a/src/size_hints.rs b/src/size_hints.rs
deleted file mode 100644
index 22543dd..0000000
--- a/src/size_hints.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-#![allow(dead_code)]
-
-pub mod flags {
- pub const P_MIN_SIZE: u32 = 1 << 4;
- pub const P_MAX_SIZE: u32 = 1 << 5;
- pub const P_RESIZE_INC: u32 = 1 << 6;
- pub const P_ASPECT: u32 = 1 << 7;
- pub const P_BASE_SIZE: u32 = 1 << 8;
-}
-
-pub mod offset {
- pub const FLAGS: usize = 0;
- pub const MIN_WIDTH: usize = 5;
- pub const MIN_HEIGHT: usize = 6;
- pub const MAX_WIDTH: usize = 7;
- pub const MAX_HEIGHT: usize = 8;
- pub const WIDTH_INC: usize = 9;
- pub const HEIGHT_INC: usize = 10;
- pub const MIN_ASPECT_X: usize = 11;
- pub const MIN_ASPECT_Y: usize = 12;
- pub const MAX_ASPECT_X: usize = 13;
- pub const MAX_ASPECT_Y: usize = 14;
- pub const BASE_WIDTH: usize = 15;
- pub const BASE_HEIGHT: usize = 16;
-}
diff --git a/src/tab_bar.rs b/src/tab_bar.rs
deleted file mode 100644
index a9835da..0000000
--- a/src/tab_bar.rs
+++ /dev/null
@@ -1,278 +0,0 @@
-use crate::ColorScheme;
-use crate::bar::font::{DrawingSurface, Font};
-use crate::errors::X11Error;
-use crate::layout::tabbed::TAB_BAR_HEIGHT;
-use x11rb::COPY_DEPTH_FROM_PARENT;
-use x11rb::connection::Connection;
-use x11rb::protocol::xproto::*;
-use x11rb::rust_connection::RustConnection;
-
-pub struct TabBar {
- window: Window,
- width: u16,
- height: u16,
- x_offset: i16,
- y_offset: i16,
- graphics_context: Gcontext,
- display: *mut x11::xlib::Display,
- surface: DrawingSurface,
- scheme_normal: ColorScheme,
- scheme_selected: ColorScheme,
-}
-
-impl TabBar {
- pub fn new(
- connection: &RustConnection,
- screen: &Screen,
- screen_num: usize,
- display: *mut x11::xlib::Display,
- _font: &Font,
- x: i16,
- y: i16,
- width: u16,
- scheme_normal: ColorScheme,
- scheme_selected: ColorScheme,
- cursor: u32,
- ) -> Result<Self, X11Error> {
- let window = connection.generate_id()?;
- let graphics_context = connection.generate_id()?;
-
- let height = TAB_BAR_HEIGHT as u16;
-
- connection.create_window(
- COPY_DEPTH_FROM_PARENT,
- window,
- screen.root,
- x,
- y,
- width,
- height,
- 0,
- WindowClass::INPUT_OUTPUT,
- screen.root_visual,
- &CreateWindowAux::new()
- .background_pixel(scheme_normal.background)
- .event_mask(EventMask::EXPOSURE | EventMask::BUTTON_PRESS)
- .override_redirect(1),
- )?;
-
- connection.create_gc(
- graphics_context,
- window,
- &CreateGCAux::new()
- .foreground(scheme_normal.foreground)
- .background(scheme_normal.background),
- )?;
-
- unsafe {
- x11::xlib::XDefineCursor(display, window as u64, cursor as u64);
- }
-
- connection.map_window(window)?;
- connection.flush()?;
-
- let visual = unsafe { x11::xlib::XDefaultVisual(display, screen_num as i32) };
- let colormap = unsafe { x11::xlib::XDefaultColormap(display, screen_num as i32) };
-
- let surface = DrawingSurface::new(
- display,
- window as x11::xlib::Drawable,
- width as u32,
- height as u32,
- visual,
- colormap,
- )?;
-
- Ok(Self {
- window,
- width,
- height,
- x_offset: x,
- y_offset: y,
- graphics_context,
- display,
- surface,
- scheme_normal,
- scheme_selected,
- })
- }
-
- pub fn window(&self) -> Window {
- self.window
- }
-
- pub fn draw(
- &mut self,
- connection: &RustConnection,
- font: &Font,
- windows: &[(Window, String)],
- focused_window: Option<Window>,
- ) -> Result<(), X11Error> {
- connection.change_gc(
- self.graphics_context,
- &ChangeGCAux::new().foreground(self.scheme_normal.background),
- )?;
- connection.flush()?;
-
- unsafe {
- let gc =
- x11::xlib::XCreateGC(self.display, self.surface.pixmap(), 0, std::ptr::null_mut());
- x11::xlib::XSetForeground(self.display, gc, self.scheme_normal.background as u64);
- x11::xlib::XFillRectangle(
- self.display,
- self.surface.pixmap(),
- gc,
- 0,
- 0,
- self.width as u32,
- self.height as u32,
- );
- x11::xlib::XFreeGC(self.display, gc);
- }
-
- if windows.is_empty() {
- self.copy_pixmap_to_window();
- return Ok(());
- }
-
- let tab_width = self.width / windows.len() as u16;
- let mut x_position: i16 = 0;
-
- for (index, &(window, ref title)) in windows.iter().enumerate() {
- let is_focused = Some(window) == focused_window;
- let scheme = if is_focused {
- &self.scheme_selected
- } else {
- &self.scheme_normal
- };
-
- let display_title = if title.is_empty() {
- format!("Window {}", index + 1)
- } else {
- title.clone()
- };
-
- let text_width = font.text_width(&display_title);
- let text_x = x_position + ((tab_width.saturating_sub(text_width)) / 2) as i16;
-
- let top_padding = 6;
- let text_y = top_padding + font.ascent();
-
- self.surface.font_draw().draw_text(
- font,
- scheme.foreground,
- text_x,
- text_y,
- &display_title,
- );
-
- if is_focused {
- let underline_height = 3;
- let underline_y = self.height as i16 - underline_height;
-
- unsafe {
- let gc = x11::xlib::XCreateGC(
- self.display,
- self.surface.pixmap(),
- 0,
- std::ptr::null_mut(),
- );
- x11::xlib::XSetForeground(self.display, gc, scheme.underline as u64);
- x11::xlib::XFillRectangle(
- self.display,
- self.surface.pixmap(),
- gc,
- x_position as i32,
- underline_y as i32,
- tab_width as u32,
- underline_height as u32,
- );
- x11::xlib::XFreeGC(self.display, gc);
- }
- }
-
- x_position += tab_width as i16;
- }
-
- self.copy_pixmap_to_window();
- Ok(())
- }
-
- fn copy_pixmap_to_window(&self) {
- unsafe {
- let gc =
- x11::xlib::XCreateGC(self.display, self.window as u64, 0, std::ptr::null_mut());
- x11::xlib::XCopyArea(
- self.display,
- self.surface.pixmap(),
- self.window as u64,
- gc,
- 0,
- 0,
- self.width as u32,
- self.height as u32,
- 0,
- 0,
- );
- x11::xlib::XFreeGC(self.display, gc);
- }
- }
-
- pub fn get_clicked_window(&self, windows: &[(Window, String)], click_x: i16) -> Option<Window> {
- if windows.is_empty() {
- return None;
- }
-
- let tab_width = self.width / windows.len() as u16;
- let tab_index = (click_x as u16 / tab_width) as usize;
-
- windows.get(tab_index).map(|&(win, _)| win)
- }
-
- pub fn reposition(
- &mut self,
- connection: &RustConnection,
- x: i16,
- y: i16,
- width: u16,
- ) -> Result<(), X11Error> {
- self.x_offset = x;
- self.y_offset = y;
- self.width = width;
-
- connection.configure_window(
- self.window,
- &ConfigureWindowAux::new()
- .x(x as i32)
- .y(y as i32)
- .width(width as u32),
- )?;
-
- let visual = unsafe { x11::xlib::XDefaultVisual(self.display, 0) };
- let colormap = unsafe { x11::xlib::XDefaultColormap(self.display, 0) };
-
- self.surface = DrawingSurface::new(
- self.display,
- self.window as x11::xlib::Drawable,
- width as u32,
- self.height as u32,
- visual,
- colormap,
- )?;
-
- connection.flush()?;
- Ok(())
- }
-
- pub fn hide(&self, connection: &RustConnection) -> Result<(), X11Error> {
- connection.unmap_window(self.window)?;
- connection.flush()?;
- Ok(())
- }
-
- pub fn show(&self, connection: &RustConnection) -> Result<(), X11Error> {
- connection.map_window(self.window)?;
- connection.flush()?;
- Ok(())
- }
-}
diff --git a/src/window_manager.rs b/src/window_manager.rs
deleted file mode 100644
index 5114c1f..0000000
--- a/src/window_manager.rs
+++ /dev/null
@@ -1,4793 +0,0 @@
-use crate::Config;
-use crate::animations::{AnimationConfig, ScrollAnimation};
-use crate::bar::Bar;
-use crate::client::{Client, TagMask};
-use crate::errors::{ConfigError, WmError};
-use crate::keyboard::{self, Arg, KeyAction, handlers};
-use crate::layout::GapConfig;
-use crate::layout::tiling::TilingLayout;
-use crate::layout::{Layout, LayoutBox, LayoutType, layout_from_str, next_layout};
-use crate::monitor::{Monitor, detect_monitors};
-use crate::overlay::{ErrorOverlay, KeybindOverlay, Overlay};
-use std::collections::{HashMap, HashSet};
-
-use x11rb::connection::Connection;
-use x11rb::protocol::Event;
-use x11rb::protocol::xproto::*;
-use x11rb::rust_connection::RustConnection;
-
-enum Control {
- Continue,
- Quit,
-}
-
-pub fn tag_mask(tag: usize) -> TagMask {
- 1 << tag
-}
-
-pub fn unmask_tag(mask: TagMask) -> usize {
- mask.trailing_zeros() as usize
-}
-
-struct AtomCache {
- net_supported: Atom,
- net_supporting_wm_check: Atom,
- net_current_desktop: Atom,
- net_client_info: Atom,
- wm_state: Atom,
- wm_protocols: Atom,
- wm_delete_window: Atom,
- net_wm_state: Atom,
- net_wm_state_fullscreen: Atom,
- net_wm_window_type: Atom,
- net_wm_window_type_dialog: Atom,
- wm_name: Atom,
- net_wm_name: Atom,
- utf8_string: Atom,
- net_active_window: Atom,
- wm_take_focus: Atom,
- net_client_list: Atom,
-}
-
-impl AtomCache {
- fn new(connection: &RustConnection) -> WmResult<Self> {
- let net_supported = connection
- .intern_atom(false, b"_NET_SUPPORTED")?
- .reply()?
- .atom;
-
- let net_supporting_wm_check = connection
- .intern_atom(false, b"_NET_SUPPORTING_WM_CHECK")?
- .reply()?
- .atom;
-
- let net_current_desktop = connection
- .intern_atom(false, b"_NET_CURRENT_DESKTOP")?
- .reply()?
- .atom;
-
- let net_client_info = connection
- .intern_atom(false, b"_NET_CLIENT_INFO")?
- .reply()?
- .atom;
-
- let wm_state = connection.intern_atom(false, b"WM_STATE")?.reply()?.atom;
-
- let wm_protocols = connection
- .intern_atom(false, b"WM_PROTOCOLS")?
- .reply()?
- .atom;
-
- let wm_delete_window = connection
- .intern_atom(false, b"WM_DELETE_WINDOW")?
- .reply()?
- .atom;
-
- let net_wm_state = connection
- .intern_atom(false, b"_NET_WM_STATE")?
- .reply()?
- .atom;
-
- let net_wm_state_fullscreen = connection
- .intern_atom(false, b"_NET_WM_STATE_FULLSCREEN")?
- .reply()?
- .atom;
-
- let net_wm_window_type = connection
- .intern_atom(false, b"_NET_WM_WINDOW_TYPE")?
- .reply()?
- .atom;
-
- let net_wm_window_type_dialog = connection
- .intern_atom(false, b"_NET_WM_WINDOW_TYPE_DIALOG")?
- .reply()?
- .atom;
-
- let wm_name = AtomEnum::WM_NAME.into();
- let net_wm_name = connection
- .intern_atom(false, b"_NET_WM_NAME")?
- .reply()?
- .atom;
- let utf8_string = connection.intern_atom(false, b"UTF8_STRING")?.reply()?.atom;
- let net_active_window = connection
- .intern_atom(false, b"_NET_ACTIVE_WINDOW")?
- .reply()?
- .atom;
-
- let wm_take_focus = connection
- .intern_atom(false, b"WM_TAKE_FOCUS")?
- .reply()?
- .atom;
-
- let net_client_list = connection
- .intern_atom(false, b"_NET_CLIENT_LIST")?
- .reply()?
- .atom;
-
- Ok(Self {
- net_supported,
- net_supporting_wm_check,
- net_current_desktop,
- net_client_info,
- wm_state,
- wm_protocols,
- wm_delete_window,
- net_wm_state,
- net_wm_state_fullscreen,
- net_wm_window_type,
- net_wm_window_type_dialog,
- wm_name,
- net_wm_name,
- utf8_string,
- net_active_window,
- wm_take_focus,
- net_client_list,
- })
- }
-}
-
-pub struct WindowManager {
- config: Config,
- connection: RustConnection,
- screen_number: usize,
- root: Window,
- _wm_check_window: Window,
- screen: Screen,
- windows: Vec<Window>,
- clients: HashMap<Window, Client>,
- layout: LayoutBox,
- gaps_enabled: bool,
- floating_windows: HashSet<Window>,
- fullscreen_windows: HashSet<Window>,
- bars: Vec<Bar>,
- tab_bars: Vec<crate::tab_bar::TabBar>,
- show_bar: bool,
- monitors: Vec<Monitor>,
- selected_monitor: usize,
- atoms: AtomCache,
- previous_focused: Option<Window>,
- display: *mut x11::xlib::Display,
- font: crate::bar::font::Font,
- keychord_state: keyboard::handlers::KeychordState,
- current_key: usize,
- keyboard_mapping: Option<keyboard::KeyboardMapping>,
- error_message: Option<String>,
- overlay: ErrorOverlay,
- keybind_overlay: KeybindOverlay,
- scroll_animation: ScrollAnimation,
- animation_config: AnimationConfig,
-}
-
-type WmResult<T> = Result<T, WmError>;
-
-impl WindowManager {
- pub fn new(config: Config) -> WmResult<Self> {
- let (connection, screen_number) = x11rb::connect(None)?;
- let root = connection.setup().roots[screen_number].root;
- let screen = connection.setup().roots[screen_number].clone();
-
- connection
- .change_window_attributes(
- root,
- &ChangeWindowAttributesAux::new().event_mask(
- EventMask::SUBSTRUCTURE_REDIRECT
- | EventMask::SUBSTRUCTURE_NOTIFY
- | EventMask::PROPERTY_CHANGE
- | EventMask::KEY_PRESS
- | EventMask::BUTTON_PRESS
- | EventMask::POINTER_MOTION,
- ),
- )?
- .check()?;
-
- let ignore_modifiers = [
- 0,
- u16::from(ModMask::LOCK),
- u16::from(ModMask::M2),
- u16::from(ModMask::LOCK | ModMask::M2),
- ];
-
- for &ignore_mask in &ignore_modifiers {
- let grab_mask = u16::from(config.modkey) | ignore_mask;
-
- connection.grab_button(
- false,
- root,
- EventMask::BUTTON_PRESS | EventMask::BUTTON_RELEASE,
- GrabMode::SYNC,
- GrabMode::ASYNC,
- x11rb::NONE,
- x11rb::NONE,
- ButtonIndex::M1,
- grab_mask.into(),
- )?;
-
- connection.grab_button(
- false,
- root,
- EventMask::BUTTON_PRESS | EventMask::BUTTON_RELEASE,
- GrabMode::SYNC,
- GrabMode::ASYNC,
- x11rb::NONE,
- x11rb::NONE,
- ButtonIndex::M3,
- grab_mask.into(),
- )?;
- }
-
- let mut monitors = detect_monitors(&connection, &screen, root)?;
- for monitor in monitors.iter_mut() {
- monitor.init_pertag(config.tags.len(), "tiling");
- }
-
- let display = unsafe { x11::xlib::XOpenDisplay(std::ptr::null()) };
- if display.is_null() {
- return Err(WmError::X11(crate::errors::X11Error::DisplayOpenFailed));
- }
-
- // C has better C interop than rust.
- let normal_cursor = unsafe { x11::xlib::XCreateFontCursor(display, 68) };
-
- unsafe {
- x11::xlib::XDefineCursor(display, root as u64, normal_cursor);
- }
-
- let font = crate::bar::font::Font::new(display, screen_number as i32, &config.font)?;
-
- let mut bars = Vec::new();
- for monitor in monitors.iter() {
- let bar = Bar::new(
- &connection,
- &screen,
- screen_number,
- &config,
- display,
- &font,
- monitor.screen_x as i16,
- monitor.screen_y as i16,
- monitor.screen_width as u16,
- normal_cursor as u32,
- )?;
- bars.push(bar);
- }
-
- let bar_height = font.height() as f32 * 1.4;
- let mut tab_bars = Vec::new();
- for monitor in monitors.iter() {
- let tab_bar = crate::tab_bar::TabBar::new(
- &connection,
- &screen,
- screen_number,
- display,
- &font,
- (monitor.screen_x + config.gap_outer_horizontal as i32) as i16,
- (monitor.screen_y as f32 + bar_height + config.gap_outer_vertical as f32) as i16,
- monitor
- .screen_width
- .saturating_sub(2 * config.gap_outer_horizontal as i32) as u16,
- config.scheme_occupied,
- config.scheme_selected,
- normal_cursor as u32,
- )?;
- tab_bars.push(tab_bar);
- }
-
- let gaps_enabled = config.gaps_enabled;
-
- let atoms = AtomCache::new(&connection)?;
-
- let supported_atoms: Vec<Atom> = vec![
- atoms.net_supported,
- atoms.net_supporting_wm_check,
- atoms.net_wm_state,
- atoms.net_wm_state_fullscreen,
- atoms.net_wm_window_type,
- atoms.net_wm_window_type_dialog,
- atoms.net_active_window,
- atoms.net_wm_name,
- atoms.net_current_desktop,
- atoms.net_client_info,
- atoms.net_client_list,
- ];
- let supported_bytes: Vec<u8> = supported_atoms
- .iter()
- .flat_map(|a| a.to_ne_bytes())
- .collect();
- connection.change_property(
- PropMode::REPLACE,
- root,
- atoms.net_supported,
- AtomEnum::ATOM,
- 32,
- supported_atoms.len() as u32,
- &supported_bytes,
- )?;
-
- let wm_check_window = connection.generate_id()?;
- connection.create_window(
- screen.root_depth,
- wm_check_window,
- root,
- 0,
- 0,
- 1,
- 1,
- 0,
- WindowClass::INPUT_OUTPUT,
- 0,
- &CreateWindowAux::new(),
- )?;
-
- connection.change_property(
- PropMode::REPLACE,
- wm_check_window,
- atoms.net_supporting_wm_check,
- AtomEnum::WINDOW,
- 32,
- 1,
- &wm_check_window.to_ne_bytes(),
- )?;
-
- connection.change_property(
- PropMode::REPLACE,
- wm_check_window,
- atoms.net_wm_name,
- atoms.utf8_string,
- 8,
- 4,
- b"oxwm",
- )?;
-
- connection.change_property(
- PropMode::REPLACE,
- root,
- atoms.net_supporting_wm_check,
- AtomEnum::WINDOW,
- 32,
- 1,
- &wm_check_window.to_ne_bytes(),
- )?;
-
- let overlay = ErrorOverlay::new(
- &connection,
- &screen,
- screen_number,
- display,
- &font,
- screen.width_in_pixels,
- )?;
-
- let keybind_overlay =
- KeybindOverlay::new(&connection, &screen, screen_number, display, config.modkey)?;
-
- let mut window_manager = Self {
- config,
- connection,
- screen_number,
- root,
- _wm_check_window: wm_check_window,
- screen,
- windows: Vec::new(),
- clients: HashMap::new(),
- layout: Box::new(TilingLayout),
- gaps_enabled,
- floating_windows: HashSet::new(),
- fullscreen_windows: HashSet::new(),
- bars,
- tab_bars,
- show_bar: true,
- monitors,
- selected_monitor: 0,
- atoms,
- previous_focused: None,
- display,
- font,
- keychord_state: keyboard::handlers::KeychordState::Idle,
- current_key: 0,
- keyboard_mapping: None,
- error_message: None,
- overlay,
- keybind_overlay,
- scroll_animation: ScrollAnimation::new(),
- animation_config: AnimationConfig::default(),
- };
-
- for tab_bar in &window_manager.tab_bars {
- tab_bar.hide(&window_manager.connection)?;
- }
-
- window_manager.scan_existing_windows()?;
- window_manager.update_bar()?;
- window_manager.run_autostart_commands();
-
- Ok(window_manager)
- }
-
- pub fn show_startup_config_error(&mut self, error: ConfigError) {
- let monitor = &self.monitors[self.selected_monitor];
- let monitor_x = monitor.screen_x as i16;
- let monitor_y = monitor.screen_y as i16;
- let screen_width = monitor.screen_width as u16;
- let screen_height = monitor.screen_height as u16;
-
- if let Err(e) = self.overlay.show_error(
- &self.connection,
- &self.font,
- error,
- monitor_x,
- monitor_y,
- screen_width,
- screen_height,
- ) {
- eprintln!("Failed to show config error overlay: {:?}", e);
- }
- }
-
- fn try_reload_config(&mut self) -> Result<(), ConfigError> {
- let lua_path = self
- .config
- .path
- .as_ref()
- .ok_or(ConfigError::NoConfigPathSet)?;
-
- if !lua_path.exists() {
- return Err(ConfigError::NoConfigAtPath);
- }
-
- let config_str =
- std::fs::read_to_string(lua_path).map_err(|e| ConfigError::CouldNotReadConfig(e))?;
-
- let config_dir = lua_path.parent();
-
- let new_config = crate::config::parse_lua_config(&config_str, config_dir)?;
-
- let lua_path = self.config.path.take();
-
- self.config = new_config;
- self.config.path = lua_path;
- self.error_message = None;
-
- for bar in &mut self.bars {
- bar.update_from_config(&self.config);
- }
-
- Ok(())
- }
-
- fn scan_existing_windows(&mut self) -> WmResult<()> {
- let tree = self.connection.query_tree(self.root)?.reply()?;
- let net_client_info = self.atoms.net_client_info;
- let wm_state_atom = self.atoms.wm_state;
-
- for &window in &tree.children {
- if self.bars.iter().any(|bar| bar.window() == window) {
- continue;
- }
-
- let Ok(attrs) = self.connection.get_window_attributes(window)?.reply() else {
- continue;
- };
-
- if attrs.override_redirect {
- continue;
- }
-
- if attrs.map_state == MapState::VIEWABLE {
- let _tag = self.get_saved_tag(window, net_client_info)?;
- self.windows.push(window);
- continue;
- }
-
- if attrs.map_state == MapState::UNMAPPED {
- let has_wm_state = self
- .connection
- .get_property(false, window, wm_state_atom, AtomEnum::ANY, 0, 2)?
- .reply()
- .is_ok_and(|prop| !prop.value.is_empty());
-
- if !has_wm_state {
- continue;
- }
-
- let has_wm_class = self
- .connection
- .get_property(false, window, AtomEnum::WM_CLASS, AtomEnum::STRING, 0, 1024)?
- .reply()
- .is_ok_and(|prop| !prop.value.is_empty());
-
- if has_wm_class {
- let _tag = self.get_saved_tag(window, net_client_info)?;
- self.connection.map_window(window)?;
- self.windows.push(window);
- }
- }
- }
-
- if let Some(&first) = self.windows.first() {
- self.focus(Some(first))?;
- }
-
- self.apply_layout()?;
- Ok(())
- }
-
- fn get_saved_tag(&self, window: Window, net_client_info: Atom) -> WmResult<TagMask> {
- match self
- .connection
- .get_property(false, window, net_client_info, AtomEnum::CARDINAL, 0, 2)?
- .reply()
- {
- Ok(prop) if prop.value.len() >= 4 => {
- let tags = u32::from_ne_bytes([
- prop.value[0],
- prop.value[1],
- prop.value[2],
- prop.value[3],
- ]);
-
- if tags != 0 && tags < (1 << self.config.tags.len()) {
- return Ok(tags);
- }
- }
- Ok(_) => {}
- Err(e) => {
- eprintln!("No _NET_CLIENT_INFO property ({})", e);
- }
- }
-
- Ok(self
- .monitors
- .get(self.selected_monitor)
- .map(|m| m.tagset[m.selected_tags_index])
- .unwrap_or(tag_mask(0)))
- }
-
- fn save_client_tag(&self, window: Window, tag: TagMask) -> WmResult<()> {
- let net_client_info = self.atoms.net_client_info;
-
- let bytes = tag.to_ne_bytes().to_vec();
-
- self.connection.change_property(
- PropMode::REPLACE,
- window,
- net_client_info,
- AtomEnum::CARDINAL,
- 32,
- 1,
- &bytes,
- )?;
-
- self.connection.flush()?;
- Ok(())
- }
-
- fn set_wm_state(&self, window: Window, state: u32) -> WmResult<()> {
- let wm_state_atom = self.atoms.wm_state;
-
- let data = [state, 0u32];
- let bytes: Vec<u8> = data.iter().flat_map(|&v| v.to_ne_bytes()).collect();
-
- self.connection.change_property(
- PropMode::REPLACE,
- window,
- wm_state_atom,
- wm_state_atom,
- 32,
- 2,
- &bytes,
- )?;
-
- self.connection.flush()?;
- Ok(())
- }
-
- fn update_client_list(&self) -> WmResult<()> {
- let window_bytes: Vec<u8> = self
- .windows
- .iter()
- .flat_map(|window| window.to_ne_bytes())
- .collect();
-
- self.connection.change_property(
- PropMode::REPLACE,
- self.root,
- self.atoms.net_client_list,
- AtomEnum::WINDOW,
- 32,
- self.windows.len() as u32,
- &window_bytes,
- )?;
-
- Ok(())
- }
-
- pub fn run(&mut self) -> WmResult<()> {
- println!("oxwm started on display {}", self.screen_number);
-
- self.grab_keys()?;
- self.update_bar()?;
-
- let mut last_bar_update = std::time::Instant::now();
- const BAR_UPDATE_INTERVAL_MS: u64 = 100;
-
- loop {
- match self.connection.poll_for_event_with_sequence()? {
- Some((event, _sequence)) => {
- if matches!(self.handle_event(event)?, Control::Quit) {
- return Ok(());
- }
- }
- None => {
- if last_bar_update.elapsed().as_millis() >= BAR_UPDATE_INTERVAL_MS as u128 {
- if let Some(bar) = self.bars.get_mut(self.selected_monitor) {
- bar.update_blocks();
- }
- if self.bars.iter().any(|bar| bar.needs_redraw()) {
- self.update_bar()?;
- }
- last_bar_update = std::time::Instant::now();
- }
-
- self.tick_animations()?;
-
- self.connection.flush()?;
- std::thread::sleep(std::time::Duration::from_millis(16));
- }
- }
- }
- }
-
- fn toggle_floating(&mut self) -> WmResult<()> {
- let focused = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- if focused.is_none() {
- return Ok(());
- }
- let focused = focused.unwrap();
-
- if let Some(client) = self.clients.get(&focused)
- && client.is_fullscreen
- {
- return Ok(());
- }
-
- let (is_fixed, x, y, w, h) = if let Some(client) = self.clients.get(&focused) {
- (
- client.is_fixed,
- client.x_position as i32,
- client.y_position as i32,
- client.width as u32,
- client.height as u32,
- )
- } else {
- return Ok(());
- };
-
- let was_floating = self.floating_windows.contains(&focused);
-
- if was_floating {
- self.floating_windows.remove(&focused);
- if let Some(client) = self.clients.get_mut(&focused) {
- client.is_floating = false;
- }
- } else {
- self.floating_windows.insert(focused);
- if let Some(client) = self.clients.get_mut(&focused) {
- client.is_floating = is_fixed || !client.is_floating;
- }
-
- self.connection.configure_window(
- focused,
- &ConfigureWindowAux::new()
- .x(x)
- .y(y)
- .width(w)
- .height(h)
- .stack_mode(StackMode::ABOVE),
- )?;
- }
-
- self.apply_layout()?;
- Ok(())
- }
-
- fn set_master_factor(&mut self, delta: f32) -> WmResult<()> {
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- let new_mfact = (monitor.master_factor + delta).clamp(0.05, 0.95);
- monitor.master_factor = new_mfact;
- if let Some(ref mut pertag) = monitor.pertag {
- pertag.master_factors[pertag.current_tag] = new_mfact;
- }
- self.apply_layout()?;
- }
- Ok(())
- }
-
- fn inc_num_master(&mut self, delta: i32) -> WmResult<()> {
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- let new_nmaster = (monitor.num_master + delta).max(0);
- monitor.num_master = new_nmaster;
- if let Some(ref mut pertag) = monitor.pertag {
- pertag.num_masters[pertag.current_tag] = new_nmaster;
- }
- self.apply_layout()?;
- }
- Ok(())
- }
-
- fn tick_animations(&mut self) -> WmResult<()> {
- if self.scroll_animation.is_active() {
- if let Some(new_offset) = self.scroll_animation.update() {
- if let Some(m) = self.monitors.get_mut(self.selected_monitor) {
- m.scroll_offset = new_offset;
- }
- self.apply_layout()?;
- self.update_bar()?;
- }
- }
- Ok(())
- }
-
- fn scroll_layout(&mut self, direction: i32) -> WmResult<()> {
- if self.layout.name() != "scrolling" {
- return Ok(());
- }
-
- let monitor_index = self.selected_monitor;
- let monitor = match self.monitors.get(monitor_index) {
- Some(m) => m.clone(),
- None => return Ok(()),
- };
-
- let visible_count = if monitor.num_master > 0 {
- monitor.num_master as usize
- } else {
- 2
- };
-
- let mut tiled_count = 0;
- let mut current = self.next_tiled(monitor.clients_head, &monitor);
- while let Some(window) = current {
- tiled_count += 1;
- if let Some(client) = self.clients.get(&window) {
- current = self.next_tiled(client.next, &monitor);
- } else {
- break;
- }
- }
-
- if tiled_count <= visible_count {
- if let Some(m) = self.monitors.get_mut(monitor_index) {
- m.scroll_offset = 0;
- }
- return Ok(());
- }
-
- let outer_gap = if self.gaps_enabled {
- self.config.gap_outer_vertical
- } else {
- 0
- };
- let inner_gap = if self.gaps_enabled {
- self.config.gap_inner_vertical
- } else {
- 0
- };
-
- let available_width = monitor.screen_width - 2 * outer_gap as i32;
- let total_inner_gaps = inner_gap as i32 * (visible_count - 1) as i32;
- let window_width = (available_width - total_inner_gaps) / visible_count as i32;
- let scroll_amount = window_width + inner_gap as i32;
-
- let total_width =
- tiled_count as i32 * window_width + (tiled_count - 1) as i32 * inner_gap as i32;
- let max_scroll = (total_width - available_width).max(0);
-
- let current_offset = monitor.scroll_offset;
- let target_offset = if self.scroll_animation.is_active() {
- self.scroll_animation.target() + direction * scroll_amount
- } else {
- current_offset + direction * scroll_amount
- };
- let target_offset = target_offset.clamp(0, max_scroll);
-
- self.scroll_animation
- .start(current_offset, target_offset, &self.animation_config);
-
- Ok(())
- }
-
- fn scroll_to_window(&mut self, target_window: Window, animate: bool) -> WmResult<()> {
- if self.layout.name() != "scrolling" {
- return Ok(());
- }
-
- let monitor_index = self.selected_monitor;
- let monitor = match self.monitors.get(monitor_index) {
- Some(m) => m.clone(),
- None => return Ok(()),
- };
-
- let visible_count = if monitor.num_master > 0 {
- monitor.num_master as usize
- } else {
- 2
- };
-
- let outer_gap = if self.gaps_enabled {
- self.config.gap_outer_vertical
- } else {
- 0
- };
- let inner_gap = if self.gaps_enabled {
- self.config.gap_inner_vertical
- } else {
- 0
- };
-
- let mut tiled_windows = Vec::new();
- let mut current = self.next_tiled(monitor.clients_head, &monitor);
- while let Some(window) = current {
- tiled_windows.push(window);
- if let Some(client) = self.clients.get(&window) {
- current = self.next_tiled(client.next, &monitor);
- } else {
- break;
- }
- }
-
- let target_idx = tiled_windows.iter().position(|&w| w == target_window);
- let target_idx = match target_idx {
- Some(idx) => idx,
- None => return Ok(()),
- };
-
- let tiled_count = tiled_windows.len();
- if tiled_count <= visible_count {
- if animate && monitor.scroll_offset != 0 {
- self.scroll_animation
- .start(monitor.scroll_offset, 0, &self.animation_config);
- } else if let Some(m) = self.monitors.get_mut(monitor_index) {
- m.scroll_offset = 0;
- }
- return Ok(());
- }
-
- let available_width = monitor.screen_width - 2 * outer_gap as i32;
- let total_inner_gaps = inner_gap as i32 * (visible_count - 1) as i32;
- let window_width = (available_width - total_inner_gaps) / visible_count as i32;
- let scroll_step = window_width + inner_gap as i32;
-
- let total_width =
- tiled_count as i32 * window_width + (tiled_count - 1) as i32 * inner_gap as i32;
- let max_scroll = (total_width - available_width).max(0);
-
- let target_scroll = (target_idx as i32) * scroll_step;
- let new_offset = target_scroll.clamp(0, max_scroll);
-
- let current_offset = monitor.scroll_offset;
- if current_offset != new_offset {
- if animate {
- self.scroll_animation
- .start(current_offset, new_offset, &self.animation_config);
- } else {
- if let Some(m) = self.monitors.get_mut(monitor_index) {
- m.scroll_offset = new_offset;
- }
- }
- }
-
- Ok(())
- }
-
- fn toggle_bar(&mut self) -> WmResult<()> {
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- monitor.show_bar = !monitor.show_bar;
- self.show_bar = monitor.show_bar;
- if let Some(ref mut pertag) = monitor.pertag {
- pertag.show_bars[pertag.current_tag] = monitor.show_bar;
- }
- }
- self.apply_layout()?;
- self.update_bar()?;
- Ok(())
- }
-
- fn get_layout_symbol(&self) -> String {
- let layout_name = self.layout.name();
-
- if layout_name == "scrolling" {
- if let Some(monitor) = self.monitors.get(self.selected_monitor) {
- let visible_count = if monitor.num_master > 0 {
- monitor.num_master as usize
- } else {
- 2
- };
-
- let mut tiled_count = 0;
- let mut current = self.next_tiled(monitor.clients_head, monitor);
- while let Some(window) = current {
- tiled_count += 1;
- if let Some(client) = self.clients.get(&window) {
- current = self.next_tiled(client.next, monitor);
- } else {
- break;
- }
- }
-
- if tiled_count > 0 {
- let outer_gap = if self.gaps_enabled {
- self.config.gap_outer_vertical
- } else {
- 0
- };
- let inner_gap = if self.gaps_enabled {
- self.config.gap_inner_vertical
- } else {
- 0
- };
-
- let available_width = monitor.screen_width - 2 * outer_gap as i32;
- let total_inner_gaps =
- inner_gap as i32 * (visible_count.min(tiled_count) - 1) as i32;
- let window_width = if tiled_count <= visible_count {
- (available_width - total_inner_gaps) / tiled_count as i32
- } else {
- (available_width - inner_gap as i32 * (visible_count - 1) as i32)
- / visible_count as i32
- };
-
- let scroll_step = window_width + inner_gap as i32;
- let first_visible = if scroll_step > 0 {
- (monitor.scroll_offset / scroll_step) + 1
- } else {
- 1
- };
- let last_visible =
- (first_visible + visible_count as i32 - 1).min(tiled_count as i32);
-
- return format!("[{}-{}/{}]", first_visible, last_visible, tiled_count);
- }
- }
- }
-
- self.config
- .layout_symbols
- .iter()
- .find(|l| l.name == layout_name)
- .map(|l| l.symbol.clone())
- .unwrap_or_else(|| self.layout.symbol().to_string())
- }
-
- fn get_keychord_indicator(&self) -> Option<String> {
- match &self.keychord_state {
- keyboard::handlers::KeychordState::Idle => None,
- keyboard::handlers::KeychordState::InProgress {
- candidates,
- keys_pressed,
- } => {
- if candidates.is_empty() {
- return None;
- }
-
- let binding = &self.config.keybindings[candidates[0]];
- let mut indicator = String::new();
-
- for (i, key_press) in binding.keys.iter().take(*keys_pressed).enumerate() {
- if i > 0 {
- indicator.push(' ');
- }
-
- for modifier in &key_press.modifiers {
- indicator.push_str(Self::format_modifier(*modifier));
- indicator.push('+');
- }
-
- indicator.push_str(&keyboard::keysyms::format_keysym(key_press.keysym));
- }
-
- indicator.push('-');
- Some(indicator)
- }
- }
- }
-
- fn format_modifier(modifier: KeyButMask) -> &'static str {
- match modifier {
- KeyButMask::MOD1 => "Alt",
- KeyButMask::MOD4 => "Super",
- KeyButMask::SHIFT => "Shift",
- KeyButMask::CONTROL => "Ctrl",
- _ => "Mod",
- }
- }
-
- fn update_bar(&mut self) -> WmResult<()> {
- let layout_symbol = self.get_layout_symbol();
- let keychord_indicator = self.get_keychord_indicator();
-
- for (monitor_index, monitor) in self.monitors.iter().enumerate() {
- if let Some(bar) = self.bars.get_mut(monitor_index) {
- let mut occupied_tags: TagMask = 0;
- let mut urgent_tags: TagMask = 0;
- for client in self.clients.values() {
- if client.monitor_index == monitor_index {
- occupied_tags |= client.tags;
- if client.is_urgent {
- urgent_tags |= client.tags;
- }
- }
- }
-
- let draw_blocks = monitor_index == self.selected_monitor;
- bar.invalidate();
- bar.draw(
- &self.connection,
- &self.font,
- self.display,
- monitor.tagset[monitor.selected_tags_index],
- occupied_tags,
- urgent_tags,
- draw_blocks,
- &layout_symbol,
- keychord_indicator.as_deref(),
- )?;
- }
- }
- Ok(())
- }
-
- fn update_tab_bars(&mut self) -> WmResult<()> {
- for (monitor_index, monitor) in self.monitors.iter().enumerate() {
- if let Some(tab_bar) = self.tab_bars.get_mut(monitor_index) {
- let visible_windows: Vec<(Window, String)> = self
- .windows
- .iter()
- .filter_map(|&window| {
- if let Some(client) = self.clients.get(&window) {
- if client.monitor_index != monitor_index
- || self.floating_windows.contains(&window)
- || self.fullscreen_windows.contains(&window)
- {
- return None;
- }
- if (client.tags & monitor.tagset[monitor.selected_tags_index]) != 0 {
- return Some((window, client.name.clone()));
- }
- }
- None
- })
- .collect();
-
- let focused_window = monitor.selected_client;
-
- tab_bar.draw(
- &self.connection,
- &self.font,
- &visible_windows,
- focused_window,
- )?;
- }
- }
- Ok(())
- }
-
- fn handle_key_action(&mut self, action: KeyAction, arg: &Arg) -> WmResult<()> {
- match action {
- KeyAction::Spawn => handlers::handle_spawn_action(action, arg, self.selected_monitor)?,
- KeyAction::SpawnTerminal => {
- crate::signal::spawn_detached(&self.config.terminal);
- }
- KeyAction::KillClient => {
- if let Some(focused) = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client)
- {
- self.kill_client(focused)?;
- }
- }
- KeyAction::ToggleFullScreen => {
- self.fullscreen()?;
- self.restack()?;
- }
- KeyAction::ChangeLayout => {
- if let Arg::Str(layout_name) = arg {
- match layout_from_str(layout_name) {
- Ok(layout) => {
- self.layout = layout;
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- if let Some(ref mut pertag) = monitor.pertag {
- pertag.layouts[pertag.current_tag] = layout_name.to_string();
- }
- }
- if layout_name != "normie" && layout_name != "floating" {
- self.floating_windows.clear();
- }
- self.apply_layout()?;
- self.update_bar()?;
- self.restack()?;
- }
- Err(e) => eprintln!("Failed to change layout: {}", e),
- }
- }
- }
- KeyAction::CycleLayout => {
- let current_name = self.layout.name();
- let next_name = next_layout(current_name);
- match layout_from_str(next_name) {
- Ok(layout) => {
- self.layout = layout;
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- if let Some(ref mut pertag) = monitor.pertag {
- pertag.layouts[pertag.current_tag] = next_name.to_string();
- }
- }
- if next_name != "normie" && next_name != "floating" {
- self.floating_windows.clear();
- }
- self.apply_layout()?;
- self.update_bar()?;
- self.restack()?;
- }
- Err(e) => eprintln!("Failed to cycle layout: {}", e),
- }
- }
- KeyAction::ToggleFloating => {
- self.toggle_floating()?;
- self.restack()?;
- }
-
- KeyAction::FocusStack => {
- if let Arg::Int(direction) = arg {
- self.focusstack(*direction)?;
- self.restack()?;
- }
- }
- KeyAction::MoveStack => {
- if let Arg::Int(direction) = arg {
- self.move_stack(*direction)?;
- self.restack()?;
- }
- }
- KeyAction::Quit | KeyAction::Restart => {}
- KeyAction::ViewTag => {
- if let Arg::Int(tag_index) = arg {
- self.view_tag(*tag_index as usize)?;
- }
- }
- KeyAction::ViewNextTag => {
- let monitor = self.get_selected_monitor();
- let current_tag_index = unmask_tag(monitor.get_selected_tag()) as i32;
- let len = self.config.tags.len() as i32;
- self.view_tag((current_tag_index + 1).rem_euclid(len) as usize)?;
- }
- KeyAction::ViewPreviousTag => {
- let monitor = self.get_selected_monitor();
- let current_tag_index = unmask_tag(monitor.get_selected_tag()) as i32;
- let len = self.config.tags.len() as i32;
- self.view_tag((current_tag_index - 1).rem_euclid(len) as usize)?;
- }
- KeyAction::ViewNextNonEmptyTag => {
- let monitor = self.get_selected_monitor();
- let current = unmask_tag(monitor.get_selected_tag()) as i32;
- let len = self.config.tags.len() as i32;
- let mon_num = monitor.monitor_number;
-
- for offset in 1..len {
- let next = (current + offset).rem_euclid(len) as usize;
- if self.has_windows_on_tag(mon_num, next) {
- self.view_tag(next)?;
- break;
- }
- }
- }
- KeyAction::ViewPreviousNonEmptyTag => {
- let monitor = self.get_selected_monitor();
- let current = unmask_tag(monitor.get_selected_tag()) as i32;
- let len = self.config.tags.len() as i32;
- let mon_num = monitor.monitor_number;
-
- for offset in 1..len {
- let prev = (current - offset).rem_euclid(len) as usize;
- if self.has_windows_on_tag(mon_num, prev) {
- self.view_tag(prev)?;
- break;
- }
- }
- }
- KeyAction::ToggleView => {
- if let Arg::Int(tag_index) = arg {
- self.toggleview(*tag_index as usize)?;
- }
- }
- KeyAction::MoveToTag => {
- if let Arg::Int(tag_index) = arg {
- self.move_to_tag(*tag_index as usize)?;
- }
- }
- KeyAction::ToggleTag => {
- if let Arg::Int(tag_index) = arg {
- self.toggletag(*tag_index as usize)?;
- }
- }
- KeyAction::ToggleGaps => {
- self.gaps_enabled = !self.gaps_enabled;
- self.apply_layout()?;
- self.restack()?;
- }
- KeyAction::FocusMonitor => {
- if let Arg::Int(direction) = arg {
- self.focus_monitor(*direction)?;
- }
- }
- KeyAction::TagMonitor => {
- if let Arg::Int(direction) = arg {
- self.send_window_to_adjacent_monitor(*direction)?;
- }
- }
- KeyAction::ShowKeybindOverlay => {
- let monitor = &self.monitors[self.selected_monitor];
- self.keybind_overlay.toggle(
- &self.connection,
- &self.font,
- &self.config.keybindings,
- monitor.screen_x as i16,
- monitor.screen_y as i16,
- monitor.screen_width as u16,
- monitor.screen_height as u16,
- )?;
- }
- KeyAction::SetMasterFactor => {
- if let Arg::Int(delta) = arg {
- self.set_master_factor(*delta as f32 / 100.0)?;
- }
- }
- KeyAction::IncNumMaster => {
- if let Arg::Int(delta) = arg {
- self.inc_num_master(*delta)?;
- }
- }
- KeyAction::ScrollLeft => {
- self.scroll_layout(-1)?;
- }
- KeyAction::ScrollRight => {
- self.scroll_layout(1)?;
- }
- KeyAction::None => {}
- }
- Ok(())
- }
-
- fn is_window_visible(&self, window: Window) -> bool {
- if let Some(client) = self.clients.get(&window) {
- let monitor = self.monitors.get(client.monitor_index);
- let selected_tags = monitor
- .map(|m| m.tagset[m.selected_tags_index])
- .unwrap_or(0);
- (client.tags & selected_tags) != 0
- } else {
- false
- }
- }
-
- fn visible_windows(&self) -> Vec<Window> {
- let mut result = Vec::new();
- for monitor in &self.monitors {
- let mut current = monitor.clients_head;
- while let Some(window) = current {
- if let Some(client) = self.clients.get(&window) {
- let visible_tags = client.tags & monitor.tagset[monitor.selected_tags_index];
- if visible_tags != 0 {
- result.push(window);
- }
- current = client.next;
- } else {
- break;
- }
- }
- }
- result
- }
-
- fn visible_windows_on_monitor(&self, monitor_index: usize) -> Vec<Window> {
- let mut result = Vec::new();
- if let Some(monitor) = self.monitors.get(monitor_index) {
- let mut current = monitor.clients_head;
- while let Some(window) = current {
- if let Some(client) = self.clients.get(&window) {
- let visible_tags = client.tags & monitor.tagset[monitor.selected_tags_index];
- if visible_tags != 0 {
- result.push(window);
- }
- current = client.next;
- } else {
- break;
- }
- }
- }
- result
- }
-
- fn get_monitor_at_point(&self, x: i32, y: i32) -> Option<usize> {
- self.monitors
- .iter()
- .position(|mon| mon.contains_point(x, y))
- }
-
- fn get_monitor_for_rect(&self, x: i32, y: i32, w: i32, h: i32) -> usize {
- let mut best_monitor = self.selected_monitor;
- let mut max_area = 0;
-
- for (idx, monitor) in self.monitors.iter().enumerate() {
- let intersect_width = 0.max(
- (x + w).min(monitor.window_area_x + monitor.window_area_width)
- - x.max(monitor.window_area_x),
- );
- let intersect_height = 0.max(
- (y + h).min(monitor.window_area_y + monitor.window_area_height)
- - y.max(monitor.window_area_y),
- );
- let area = intersect_width * intersect_height;
-
- if area > max_area {
- max_area = area;
- best_monitor = idx;
- }
- }
-
- best_monitor
- }
-
- fn move_window_to_monitor(
- &mut self,
- window: Window,
- target_monitor_index: usize,
- ) -> WmResult<()> {
- let current_monitor_index = self.clients.get(&window).map(|c| c.monitor_index);
-
- if let Some(current_idx) = current_monitor_index
- && current_idx == target_monitor_index
- {
- return Ok(());
- }
-
- self.unfocus(window, false)?;
- self.detach(window);
- self.detach_stack(window);
-
- if let Some(client) = self.clients.get_mut(&window) {
- client.monitor_index = target_monitor_index;
- if let Some(target_monitor) = self.monitors.get(target_monitor_index) {
- client.tags = target_monitor.tagset[target_monitor.selected_tags_index];
- }
- }
-
- self.attach_aside(window, target_monitor_index);
- self.attach_stack(window, target_monitor_index);
-
- self.focus(None)?;
- self.apply_layout()?;
-
- Ok(())
- }
-
- fn get_adjacent_monitor(&self, direction: i32) -> Option<usize> {
- if self.monitors.len() <= 1 {
- return None;
- }
-
- if direction > 0 {
- if self.selected_monitor + 1 < self.monitors.len() {
- Some(self.selected_monitor + 1)
- } else {
- Some(0)
- }
- } else if self.selected_monitor == 0 {
- Some(self.monitors.len() - 1)
- } else {
- Some(self.selected_monitor - 1)
- }
- }
-
- fn is_visible(&self, window: Window) -> bool {
- let Some(client) = self.clients.get(&window) else {
- return false;
- };
-
- let Some(monitor) = self.monitors.get(client.monitor_index) else {
- return false;
- };
-
- (client.tags & monitor.tagset[monitor.selected_tags_index]) != 0
- }
-
- fn showhide(&mut self, window: Option<Window>) -> WmResult<()> {
- let Some(window) = window else {
- return Ok(());
- };
-
- let Some(client) = self.clients.get(&window).cloned() else {
- return Ok(());
- };
-
- let monitor = match self.monitors.get(client.monitor_index) {
- Some(m) => m,
- None => return Ok(()),
- };
-
- let is_visible = (client.tags & monitor.tagset[monitor.selected_tags_index]) != 0;
-
- if is_visible {
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .x(client.x_position as i32)
- .y(client.y_position as i32),
- )?;
-
- let is_floating = client.is_floating;
- let is_fullscreen = client.is_fullscreen;
- let has_no_layout = self.layout.name() == LayoutType::Normie.as_str();
-
- if (has_no_layout || is_floating) && !is_fullscreen {
- let (x, y, w, h, changed) = self.apply_size_hints(
- window,
- client.x_position as i32,
- client.y_position as i32,
- client.width as i32,
- client.height as i32,
- );
- if changed {
- if let Some(c) = self.clients.get_mut(&window) {
- c.old_x_position = c.x_position;
- c.old_y_position = c.y_position;
- c.old_width = c.width;
- c.old_height = c.height;
- c.x_position = x as i16;
- c.y_position = y as i16;
- c.width = w as u16;
- c.height = h as u16;
- }
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .x(x)
- .y(y)
- .width(w as u32)
- .height(h as u32)
- .border_width(self.config.border_width),
- )?;
- self.send_configure_notify(window)?;
- self.connection.flush()?;
- }
- }
-
- self.showhide(client.stack_next)?;
- } else {
- self.showhide(client.stack_next)?;
-
- let width = client.width_with_border() as i32;
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .x(width * -2)
- .y(client.y_position as i32),
- )?;
- }
-
- Ok(())
- }
-
- pub fn view_tag(&mut self, tag_index: usize) -> WmResult<()> {
- if tag_index >= self.config.tags.len() {
- return Ok(());
- }
-
- let new_tagset = tag_mask(tag_index);
- let mut layout_name: Option<String> = None;
- let mut toggle_bar = false;
-
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- if new_tagset == monitor.tagset[monitor.selected_tags_index] {
- if !self.config.tag_back_and_forth {
- return Ok(());
- }
- monitor.tagset.swap(0, 1);
- if let Some(ref mut pertag) = monitor.pertag {
- let tmp = pertag.previous_tag;
- pertag.previous_tag = pertag.current_tag;
- pertag.current_tag = tmp;
- }
- } else {
- monitor.selected_tags_index ^= 1;
- monitor.tagset[monitor.selected_tags_index] = new_tagset;
- if let Some(ref mut pertag) = monitor.pertag {
- pertag.previous_tag = pertag.current_tag;
- pertag.current_tag = tag_index + 1;
- }
- }
-
- if let Some(ref pertag) = monitor.pertag {
- monitor.num_master = pertag.num_masters[pertag.current_tag];
- monitor.master_factor = pertag.master_factors[pertag.current_tag];
- layout_name = Some(pertag.layouts[pertag.current_tag].clone());
- if monitor.show_bar != pertag.show_bars[pertag.current_tag] {
- toggle_bar = true;
- }
- }
- }
-
- if let Some(name) = layout_name {
- if let Ok(layout) = layout_from_str(&name) {
- self.layout = layout;
- }
- }
-
- if toggle_bar {
- self.toggle_bar()?;
- }
-
- self.save_selected_tags()?;
- self.focus(None)?;
- self.apply_layout()?;
- self.update_bar()?;
-
- Ok(())
- }
-
- pub fn toggleview(&mut self, tag_index: usize) -> WmResult<()> {
- if tag_index >= self.config.tags.len() {
- return Ok(());
- }
-
- let num_tags = self.config.tags.len();
- let all_tags_mask = (1u32 << num_tags) - 1;
- let mut layout_name: Option<String> = None;
- let mut toggle_bar = false;
-
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- let mask = tag_mask(tag_index);
- let new_tagset = monitor.tagset[monitor.selected_tags_index] ^ mask;
-
- if new_tagset == 0 {
- return Ok(());
- }
-
- monitor.tagset[monitor.selected_tags_index] = new_tagset;
-
- if let Some(ref mut pertag) = monitor.pertag {
- if new_tagset == all_tags_mask {
- pertag.previous_tag = pertag.current_tag;
- pertag.current_tag = 0;
- }
-
- if pertag.current_tag > 0 && (new_tagset & (1 << (pertag.current_tag - 1))) == 0 {
- pertag.previous_tag = pertag.current_tag;
- pertag.current_tag = (new_tagset.trailing_zeros() as usize) + 1;
- }
-
- monitor.num_master = pertag.num_masters[pertag.current_tag];
- monitor.master_factor = pertag.master_factors[pertag.current_tag];
- layout_name = Some(pertag.layouts[pertag.current_tag].clone());
- if monitor.show_bar != pertag.show_bars[pertag.current_tag] {
- toggle_bar = true;
- }
- }
- }
-
- if let Some(name) = layout_name {
- if let Ok(layout) = layout_from_str(&name) {
- self.layout = layout;
- }
- }
-
- if toggle_bar {
- self.toggle_bar()?;
- }
-
- self.save_selected_tags()?;
- self.focus(None)?;
- self.apply_layout()?;
- self.update_bar()?;
-
- Ok(())
- }
-
- fn save_selected_tags(&self) -> WmResult<()> {
- let net_current_desktop = self.atoms.net_current_desktop;
-
- let selected_tags = self
- .monitors
- .get(self.selected_monitor)
- .map(|m| m.tagset[m.selected_tags_index])
- .unwrap_or(tag_mask(0));
- let desktop = selected_tags.trailing_zeros();
-
- let bytes = (desktop as u32).to_ne_bytes();
- self.connection.change_property(
- PropMode::REPLACE,
- self.root,
- net_current_desktop,
- AtomEnum::CARDINAL,
- 32,
- 1,
- &bytes,
- )?;
-
- self.connection.flush()?;
- Ok(())
- }
-
- pub fn move_to_tag(&mut self, tag_index: usize) -> WmResult<()> {
- if tag_index >= self.config.tags.len() {
- return Ok(());
- }
-
- let focused = match self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client)
- {
- Some(win) => win,
- None => return Ok(()),
- };
-
- let mask = tag_mask(tag_index);
-
- if let Some(client) = self.clients.get_mut(&focused) {
- client.tags = mask;
- }
-
- if let Err(error) = self.save_client_tag(focused, mask) {
- eprintln!("Failed to save client tag: {:?}", error);
- }
-
- self.focus(None)?;
- self.apply_layout()?;
- self.update_bar()?;
-
- Ok(())
- }
-
- pub fn toggletag(&mut self, tag_index: usize) -> WmResult<()> {
- if tag_index >= self.config.tags.len() {
- return Ok(());
- }
-
- let focused = match self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client)
- {
- Some(win) => win,
- None => return Ok(()),
- };
-
- let mask = tag_mask(tag_index);
- let current_tags = self.clients.get(&focused).map(|c| c.tags).unwrap_or(0);
- let new_tags = current_tags ^ mask;
-
- if new_tags == 0 {
- return Ok(());
- }
-
- if let Some(client) = self.clients.get_mut(&focused) {
- client.tags = new_tags;
- }
-
- if let Err(error) = self.save_client_tag(focused, new_tags) {
- eprintln!("Failed to save client tag: {:?}", error);
- }
-
- self.focus(None)?;
- self.apply_layout()?;
- self.update_bar()?;
-
- Ok(())
- }
-
- pub fn cycle_focus(&mut self, direction: i32) -> WmResult<()> {
- let visible = self.visible_windows();
-
- if visible.is_empty() {
- return Ok(());
- }
-
- let current = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- let next_window = if let Some(current) = current {
- if let Some(current_index) = visible.iter().position(|&w| w == current) {
- let next_index = if direction > 0 {
- (current_index + 1) % visible.len()
- } else {
- (current_index + visible.len() - 1) % visible.len()
- };
- visible[next_index]
- } else {
- visible[0]
- }
- } else {
- visible[0]
- };
-
- let is_tabbed = self.layout.name() == "tabbed";
- if is_tabbed {
- self.connection.configure_window(
- next_window,
- &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
- )?;
- }
-
- self.focus(Some(next_window))?;
-
- if is_tabbed {
- self.update_tab_bars()?;
- }
-
- Ok(())
- }
-
- fn grab_keys(&mut self) -> WmResult<()> {
- self.keyboard_mapping = Some(keyboard::grab_keys(
- &self.connection,
- self.root,
- &self.config.keybindings,
- self.current_key,
- )?);
- Ok(())
- }
-
- fn kill_client(&self, window: Window) -> WmResult<()> {
- if self.send_event(window, self.atoms.wm_delete_window)? {
- self.connection.flush()?;
- } else {
- eprintln!(
- "Window {} doesn't support WM_DELETE_WINDOW, killing forcefully",
- window
- );
- self.connection.kill_client(window)?;
- self.connection.flush()?;
- }
- Ok(())
- }
-
- fn send_event(&self, window: Window, protocol: Atom) -> WmResult<bool> {
- let protocols_reply = self
- .connection
- .get_property(
- false,
- window,
- self.atoms.wm_protocols,
- AtomEnum::ATOM,
- 0,
- 100,
- )?
- .reply();
-
- let protocols_reply = match protocols_reply {
- Ok(reply) => reply,
- Err(_) => return Ok(false),
- };
-
- let protocols: Vec<Atom> = protocols_reply
- .value
- .chunks_exact(4)
- .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
- .collect();
-
- if !protocols.contains(&protocol) {
- return Ok(false);
- }
-
- let event = x11rb::protocol::xproto::ClientMessageEvent {
- response_type: x11rb::protocol::xproto::CLIENT_MESSAGE_EVENT,
- format: 32,
- sequence: 0,
- window,
- type_: self.atoms.wm_protocols,
- data: x11rb::protocol::xproto::ClientMessageData::from([
- protocol,
- x11rb::CURRENT_TIME,
- 0,
- 0,
- 0,
- ]),
- };
-
- self.connection
- .send_event(false, window, EventMask::NO_EVENT, event)?;
- self.connection.flush()?;
- Ok(true)
- }
-
- fn set_urgent(&mut self, window: Window, urgent: bool) -> WmResult<()> {
- if let Some(client) = self.clients.get_mut(&window) {
- client.is_urgent = urgent;
- }
-
- let hints_reply = self
- .connection
- .get_property(false, window, AtomEnum::WM_HINTS, AtomEnum::WM_HINTS, 0, 9)?
- .reply();
-
- if let Ok(hints) = hints_reply
- && hints.value.len() >= 4
- {
- let mut flags = u32::from_ne_bytes([
- hints.value[0],
- hints.value[1],
- hints.value[2],
- hints.value[3],
- ]);
-
- if urgent {
- flags |= 256;
- } else {
- flags &= !256;
- }
-
- let mut new_hints = hints.value.clone();
- new_hints[0..4].copy_from_slice(&flags.to_ne_bytes());
-
- self.connection.change_property(
- PropMode::REPLACE,
- window,
- AtomEnum::WM_HINTS,
- AtomEnum::WM_HINTS,
- 32,
- new_hints.len() as u32 / 4,
- &new_hints,
- )?;
- }
-
- Ok(())
- }
-
- fn get_window_atom_property(&self, window: Window, property: Atom) -> WmResult<Option<Atom>> {
- let reply = self
- .connection
- .get_property(false, window, property, AtomEnum::ATOM, 0, 1)?
- .reply();
-
- match reply {
- Ok(prop) if !prop.value.is_empty() && prop.value.len() >= 4 => {
- let atom = u32::from_ne_bytes([
- prop.value[0],
- prop.value[1],
- prop.value[2],
- prop.value[3],
- ]);
- Ok(Some(atom))
- }
- _ => Ok(None),
- }
- }
-
- fn get_window_atom_list_property(&self, window: Window, property: Atom) -> WmResult<Vec<Atom>> {
- let reply = self
- .connection
- .get_property(false, window, property, AtomEnum::ATOM, 0, 32)?
- .reply();
-
- match reply {
- Ok(prop) if !prop.value.is_empty() => {
- let atoms: Vec<Atom> = prop
- .value
- .chunks_exact(4)
- .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
- .collect();
- Ok(atoms)
- }
- _ => Ok(Vec::new()),
- }
- }
-
- fn fullscreen(&mut self) -> WmResult<()> {
- let Some(focused_window) = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client)
- else {
- return Ok(());
- };
-
- let is_fullscreen = self.fullscreen_windows.contains(&focused_window);
- self.set_window_fullscreen(focused_window, !is_fullscreen)?;
- Ok(())
- }
-
- fn set_window_fullscreen(&mut self, window: Window, fullscreen: bool) -> WmResult<()> {
- let monitor_idx = self
- .clients
- .get(&window)
- .map(|c| c.monitor_index)
- .unwrap_or(self.selected_monitor);
- let monitor = &self.monitors[monitor_idx];
-
- if fullscreen && !self.fullscreen_windows.contains(&window) {
- let bytes = self.atoms.net_wm_state_fullscreen.to_ne_bytes().to_vec();
- self.connection.change_property(
- PropMode::REPLACE,
- window,
- self.atoms.net_wm_state,
- AtomEnum::ATOM,
- 32,
- 1,
- &bytes,
- )?;
-
- if let Some(client) = self.clients.get_mut(&window) {
- client.is_fullscreen = true;
- client.old_state = client.is_floating;
- client.old_border_width = client.border_width;
- client.old_x_position = client.x_position;
- client.old_y_position = client.y_position;
- client.old_width = client.width;
- client.old_height = client.height;
- client.border_width = 0;
- client.is_floating = true;
- }
-
- self.fullscreen_windows.insert(window);
- self.floating_windows.insert(window);
-
- self.connection.configure_window(
- window,
- &x11rb::protocol::xproto::ConfigureWindowAux::new()
- .border_width(0)
- .x(monitor.screen_x)
- .y(monitor.screen_y)
- .width(monitor.screen_width as u32)
- .height(monitor.screen_height as u32)
- .stack_mode(x11rb::protocol::xproto::StackMode::ABOVE),
- )?;
-
- self.connection.flush()?;
- } else if !fullscreen && self.fullscreen_windows.contains(&window) {
- self.connection.change_property(
- PropMode::REPLACE,
- window,
- self.atoms.net_wm_state,
- AtomEnum::ATOM,
- 32,
- 0,
- &[],
- )?;
-
- self.fullscreen_windows.remove(&window);
-
- let (
- was_floating,
- restored_x,
- restored_y,
- restored_width,
- restored_height,
- restored_border,
- ) = self
- .clients
- .get(&window)
- .map(|client| {
- (
- client.old_state,
- client.old_x_position,
- client.old_y_position,
- client.old_width,
- client.old_height,
- client.old_border_width,
- )
- })
- .unwrap_or((false, 0, 0, 100, 100, 0));
-
- if !was_floating {
- self.floating_windows.remove(&window);
- }
-
- if let Some(client) = self.clients.get_mut(&window) {
- client.is_fullscreen = false;
- client.is_floating = client.old_state;
- client.border_width = client.old_border_width;
- client.x_position = client.old_x_position;
- client.y_position = client.old_y_position;
- client.width = client.old_width;
- client.height = client.old_height;
- }
-
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .x(restored_x as i32)
- .y(restored_y as i32)
- .width(restored_width as u32)
- .height(restored_height as u32)
- .border_width(restored_border as u32),
- )?;
-
- self.apply_layout()?;
- }
-
- Ok(())
- }
-
- fn get_transient_parent(&self, window: Window) -> Option<Window> {
- self.connection
- .get_property(
- false,
- window,
- AtomEnum::WM_TRANSIENT_FOR,
- AtomEnum::WINDOW,
- 0,
- 1,
- )
- .ok()
- .and_then(|cookie| cookie.reply().ok())
- .filter(|reply| !reply.value.is_empty())
- .and_then(|reply| {
- if reply.value.len() >= 4 {
- let parent_window = u32::from_ne_bytes([
- reply.value[0],
- reply.value[1],
- reply.value[2],
- reply.value[3],
- ]);
- Some(parent_window)
- } else {
- None
- }
- })
- }
-
- fn get_window_class_instance(&self, window: Window) -> (String, String) {
- let reply = self
- .connection
- .get_property(false, window, AtomEnum::WM_CLASS, AtomEnum::STRING, 0, 1024)
- .ok()
- .and_then(|cookie| cookie.reply().ok());
-
- if let Some(reply) = reply
- && !reply.value.is_empty()
- && let Ok(text) = std::str::from_utf8(&reply.value)
- {
- let parts: Vec<&str> = text.split('\0').collect();
- let instance = parts.first().unwrap_or(&"").to_string();
- let class = parts.get(1).unwrap_or(&"").to_string();
- return (instance, class);
- }
-
- (String::new(), String::new())
- }
-
- fn apply_rules(&mut self, window: Window) -> WmResult<()> {
- let (instance, class) = self.get_window_class_instance(window);
- let title = self
- .clients
- .get(&window)
- .map(|c| c.name.clone())
- .unwrap_or_default();
-
- let mut rule_tags: Option<u32> = None;
- let mut rule_floating: Option<bool> = None;
- let mut rule_monitor: Option<usize> = None;
- let mut rule_focus = false;
-
- for rule in &self.config.window_rules {
- if rule.matches(&class, &instance, &title) {
- if rule.tags.is_some() {
- rule_tags = rule.tags;
- }
- if rule.is_floating.is_some() {
- rule_floating = rule.is_floating;
- }
- if rule.monitor.is_some() {
- rule_monitor = rule.monitor;
- }
- rule_focus = rule.focus.unwrap_or(false);
- }
- }
-
- if let Some(client) = self.clients.get_mut(&window) {
- if let Some(is_floating) = rule_floating {
- client.is_floating = is_floating;
- if is_floating {
- self.floating_windows.insert(window);
- } else {
- self.floating_windows.remove(&window);
- }
- }
-
- if let Some(monitor_index) = rule_monitor
- && monitor_index < self.monitors.len()
- {
- client.monitor_index = monitor_index;
- }
-
- if let Some(tags) = rule_tags {
- client.tags = tags;
-
- if rule_focus {
- let tag_index = unmask_tag(tags);
- let monitor_tagset = self
- .monitors
- .get(client.monitor_index)
- .map(|monitor| monitor.get_selected_tag())
- .unwrap_or(tag_mask(0));
- let is_tag_focused = monitor_tagset & tags == tags;
-
- if !is_tag_focused {
- self.view_tag(tag_index)?;
- }
- }
- }
- }
-
- Ok(())
- }
-
- fn manage_window(&mut self, window: Window) -> WmResult<()> {
- let geometry = self.connection.get_geometry(window)?.reply()?;
- let border_width = self.config.border_width;
-
- let transient_parent = self.get_transient_parent(window);
- let is_transient = transient_parent.is_some();
-
- let (monitor_index, tags) = if let Some(parent) = transient_parent {
- if let Some(parent_client) = self.clients.get(&parent) {
- (parent_client.monitor_index, parent_client.tags)
- } else {
- let tags = self
- .monitors
- .get(self.selected_monitor)
- .map(|monitor| monitor.tagset[monitor.selected_tags_index])
- .unwrap_or(tag_mask(0));
- (self.selected_monitor, tags)
- }
- } else {
- let tags = self
- .monitors
- .get(self.selected_monitor)
- .map(|monitor| monitor.tagset[monitor.selected_tags_index])
- .unwrap_or(tag_mask(0));
- (self.selected_monitor, tags)
- };
-
- let mut client = Client::new(window, monitor_index, tags);
- client.x_position = geometry.x;
- client.y_position = geometry.y;
- client.width = geometry.width;
- client.height = geometry.height;
- client.old_x_position = geometry.x;
- client.old_y_position = geometry.y;
- client.old_width = geometry.width;
- client.old_height = geometry.height;
- client.old_border_width = geometry.border_width;
- client.border_width = border_width as u16;
-
- self.clients.insert(window, client);
- self.update_window_title(window)?;
-
- if !is_transient {
- self.apply_rules(window)?;
- }
-
- let client_monitor = self
- .clients
- .get(&window)
- .map(|c| c.monitor_index)
- .unwrap_or(monitor_index);
- let monitor = &self.monitors[client_monitor];
-
- let mut x = self
- .clients
- .get(&window)
- .map(|c| c.x_position as i32)
- .unwrap_or(0);
- let mut y = self
- .clients
- .get(&window)
- .map(|c| c.y_position as i32)
- .unwrap_or(0);
- let w = self
- .clients
- .get(&window)
- .map(|c| c.width as i32)
- .unwrap_or(1);
- let h = self
- .clients
- .get(&window)
- .map(|c| c.height as i32)
- .unwrap_or(1);
- let bw = border_width as i32;
-
- if x + w + 2 * bw > monitor.window_area_x + monitor.window_area_width {
- x = monitor.window_area_x + monitor.window_area_width - w - 2 * bw;
- }
- if y + h + 2 * bw > monitor.window_area_y + monitor.window_area_height {
- y = monitor.window_area_y + monitor.window_area_height - h - 2 * bw;
- }
- x = x.max(monitor.window_area_x);
- y = y.max(monitor.window_area_y);
-
- if let Some(c) = self.clients.get_mut(&window) {
- c.x_position = x as i16;
- c.y_position = y as i16;
- }
-
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new().border_width(border_width),
- )?;
- self.connection.change_window_attributes(
- window,
- &ChangeWindowAttributesAux::new().border_pixel(self.config.border_unfocused),
- )?;
- self.send_configure_notify(window)?;
- self.update_window_type(window)?;
- self.update_size_hints(window)?;
- self.update_window_hints(window)?;
-
- self.connection.change_window_attributes(
- window,
- &ChangeWindowAttributesAux::new().event_mask(
- EventMask::ENTER_WINDOW
- | EventMask::FOCUS_CHANGE
- | EventMask::PROPERTY_CHANGE
- | EventMask::STRUCTURE_NOTIFY,
- ),
- )?;
-
- let is_fixed = self
- .clients
- .get(&window)
- .map(|c| c.is_fixed)
- .unwrap_or(false);
- if let Some(c) = self.clients.get_mut(&window)
- && !c.is_floating
- {
- c.is_floating = is_transient || is_fixed;
- c.old_state = c.is_floating;
- }
-
- if self
- .clients
- .get(&window)
- .map(|c| c.is_floating)
- .unwrap_or(false)
- {
- self.floating_windows.insert(window);
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
- )?;
- }
-
- if self.layout.name() == "scrolling" {
- if let Some(selected) = self
- .monitors
- .get(client_monitor)
- .and_then(|m| m.selected_client)
- {
- self.attach_after(window, selected, client_monitor);
- } else {
- self.attach_aside(window, client_monitor);
- }
- } else {
- self.attach_aside(window, client_monitor);
- }
- self.attach_stack(window, client_monitor);
- self.windows.push(window);
-
- let off_screen_x = x + 2 * self.screen.width_in_pixels as i32;
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .x(off_screen_x)
- .y(y)
- .width(w as u32)
- .height(h as u32),
- )?;
-
- self.set_wm_state(window, 1)?;
- self.update_client_list()?;
-
- let final_tags = self.clients.get(&window).map(|c| c.tags).unwrap_or(tags);
- let _ = self.save_client_tag(window, final_tags);
-
- if client_monitor == self.selected_monitor
- && let Some(old_sel) = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client)
- {
- self.unfocus(old_sel, false)?;
- }
-
- if let Some(m) = self.monitors.get_mut(client_monitor) {
- m.selected_client = Some(window);
- }
-
- if self.layout.name() == "scrolling" {
- self.scroll_to_window(window, true)?;
- }
-
- self.apply_layout()?;
- self.connection.map_window(window)?;
- self.focus(None)?;
- self.update_bar()?;
-
- if self.layout.name() == "tabbed" {
- self.update_tab_bars()?;
- }
-
- Ok(())
- }
-
- pub fn set_focus(&mut self, window: Window) -> WmResult<()> {
- let never_focus = self
- .clients
- .get(&window)
- .map(|c| c.never_focus)
- .unwrap_or(false);
-
- if !never_focus {
- self.connection.set_input_focus(
- InputFocus::POINTER_ROOT,
- window,
- x11rb::CURRENT_TIME,
- )?;
-
- self.connection.change_property(
- PropMode::REPLACE,
- self.root,
- self.atoms.net_active_window,
- AtomEnum::WINDOW,
- 32,
- 1,
- &window.to_ne_bytes(),
- )?;
- }
-
- let _ = self.send_event(window, self.atoms.wm_take_focus);
- self.connection.flush()?;
-
- Ok(())
- }
-
- fn grabbuttons(&self, window: Window, focused: bool) -> WmResult<()> {
- self.connection
- .ungrab_button(ButtonIndex::ANY, window, ModMask::ANY)?;
-
- if !focused {
- self.connection.grab_button(
- false,
- window,
- EventMask::BUTTON_PRESS | EventMask::BUTTON_RELEASE,
- GrabMode::SYNC,
- GrabMode::SYNC,
- x11rb::NONE,
- x11rb::NONE,
- ButtonIndex::ANY,
- ModMask::ANY,
- )?;
- }
-
- let ignore_modifiers = [
- 0u16,
- u16::from(ModMask::LOCK),
- u16::from(ModMask::M2),
- u16::from(ModMask::LOCK | ModMask::M2),
- ];
-
- for &ignore_mask in &ignore_modifiers {
- let grab_mask = u16::from(self.config.modkey) | ignore_mask;
-
- self.connection.grab_button(
- false,
- window,
- EventMask::BUTTON_PRESS | EventMask::BUTTON_RELEASE,
- GrabMode::ASYNC,
- GrabMode::SYNC,
- x11rb::NONE,
- x11rb::NONE,
- ButtonIndex::M1,
- grab_mask.into(),
- )?;
-
- self.connection.grab_button(
- false,
- window,
- EventMask::BUTTON_PRESS | EventMask::BUTTON_RELEASE,
- GrabMode::ASYNC,
- GrabMode::SYNC,
- x11rb::NONE,
- x11rb::NONE,
- ButtonIndex::M3,
- grab_mask.into(),
- )?;
- }
-
- Ok(())
- }
-
- fn unfocus(&self, window: Window, reset_input_focus: bool) -> WmResult<()> {
- if !self.windows.contains(&window) {
- return Ok(());
- }
-
- self.grabbuttons(window, false)?;
-
- self.connection.change_window_attributes(
- window,
- &ChangeWindowAttributesAux::new().border_pixel(self.config.border_unfocused),
- )?;
-
- if reset_input_focus {
- self.connection.set_input_focus(
- InputFocus::POINTER_ROOT,
- self.root,
- x11rb::CURRENT_TIME,
- )?;
- self.connection
- .delete_property(self.root, self.atoms.net_active_window)?;
- }
-
- Ok(())
- }
-
- fn focus(&mut self, window: Option<Window>) -> WmResult<()> {
- let old_selected = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- let mut focus_client = window;
- if focus_client.is_none() || focus_client.is_some_and(|w| !self.is_visible(w)) {
- let mut current = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.stack_head);
-
- focus_client = None;
- while let Some(w) = current {
- if self.is_visible(w) {
- focus_client = Some(w);
- break;
- }
- current = self.clients.get(&w).and_then(|c| c.stack_next);
- }
- }
-
- if old_selected != focus_client {
- if let Some(old_win) = old_selected {
- self.unfocus(old_win, false)?;
- }
- }
-
- if let Some(win) = focus_client {
- let monitor_idx = self
- .clients
- .get(&win)
- .map(|c| c.monitor_index)
- .unwrap_or(self.selected_monitor);
-
- if monitor_idx != self.selected_monitor {
- self.selected_monitor = monitor_idx;
- }
-
- if self.clients.get(&win).is_some_and(|c| c.is_urgent) {
- self.set_urgent(win, false)?;
- }
-
- self.detach_stack(win);
- self.attach_stack(win, monitor_idx);
-
- self.grabbuttons(win, true)?;
-
- self.connection.change_window_attributes(
- win,
- &ChangeWindowAttributesAux::new().border_pixel(self.config.border_focused),
- )?;
-
- let never_focus = self
- .clients
- .get(&win)
- .map(|client| client.never_focus)
- .unwrap_or(false);
-
- if !never_focus {
- self.connection.set_input_focus(
- InputFocus::POINTER_ROOT,
- win,
- x11rb::CURRENT_TIME,
- )?;
-
- self.connection.change_property(
- PropMode::REPLACE,
- self.root,
- self.atoms.net_active_window,
- AtomEnum::WINDOW,
- 32,
- 1,
- &win.to_ne_bytes(),
- )?;
- }
-
- let _ = self.send_event(win, self.atoms.wm_take_focus);
-
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- monitor.selected_client = Some(win);
- }
-
- self.previous_focused = Some(win);
- } else {
- self.connection.set_input_focus(
- InputFocus::POINTER_ROOT,
- self.root,
- x11rb::CURRENT_TIME,
- )?;
-
- self.connection
- .delete_property(self.root, self.atoms.net_active_window)?;
-
- if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- monitor.selected_client = None;
- }
- }
-
- self.connection.flush()?;
-
- Ok(())
- }
-
- fn restack(&mut self) -> WmResult<()> {
- let monitor = match self.monitors.get(self.selected_monitor) {
- Some(m) => m,
- None => return Ok(()),
- };
-
- let mut windows_to_restack: Vec<Window> = Vec::new();
-
- if let Some(selected) = monitor.selected_client
- && self.floating_windows.contains(&selected)
- {
- windows_to_restack.push(selected);
- }
-
- let mut current = monitor.stack_head;
- while let Some(win) = current {
- if self.windows.contains(&win)
- && self.floating_windows.contains(&win)
- && Some(win) != monitor.selected_client
- {
- windows_to_restack.push(win);
- }
- current = self.clients.get(&win).and_then(|c| c.stack_next);
- }
-
- current = monitor.stack_head;
- while let Some(win) = current {
- if self.windows.contains(&win) && !self.floating_windows.contains(&win) {
- windows_to_restack.push(win);
- }
- current = self.clients.get(&win).and_then(|c| c.stack_next);
- }
-
- for (i, &win) in windows_to_restack.iter().enumerate() {
- if i == 0 {
- self.connection.configure_window(
- win,
- &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
- )?;
- } else {
- self.connection.configure_window(
- win,
- &ConfigureWindowAux::new()
- .sibling(windows_to_restack[i - 1])
- .stack_mode(StackMode::BELOW),
- )?;
- }
- }
-
- Ok(())
- }
-
- fn focusstack(&mut self, direction: i32) -> WmResult<()> {
- let monitor = match self.monitors.get(self.selected_monitor) {
- Some(monitor) => monitor,
- None => return Ok(()),
- };
-
- let selected_window = match monitor.selected_client {
- Some(window) => window,
- None => return Ok(()),
- };
-
- let selected_is_fullscreen = self
- .clients
- .get(&selected_window)
- .map(|client| client.is_fullscreen)
- .unwrap_or(false);
-
- if selected_is_fullscreen {
- return Ok(());
- }
-
- let selected_tags = monitor.tagset[monitor.selected_tags_index];
-
- let mut stack_windows: Vec<Window> = Vec::new();
- let mut current_window = monitor.clients_head;
- while let Some(window) = current_window {
- if let Some(client) = self.clients.get(&window) {
- if client.tags & selected_tags != 0 && !client.is_floating {
- stack_windows.push(window);
- }
- current_window = client.next;
- } else {
- break;
- }
- }
-
- if stack_windows.is_empty() {
- return Ok(());
- }
-
- let current_index = stack_windows
- .iter()
- .position(|&window| window == selected_window);
-
- let next_window = if let Some(index) = current_index {
- if direction > 0 {
- if index + 1 < stack_windows.len() {
- stack_windows[index + 1]
- } else {
- stack_windows[0]
- }
- } else if index > 0 {
- stack_windows[index - 1]
- } else {
- stack_windows[stack_windows.len() - 1]
- }
- } else {
- return Ok(());
- };
-
- self.focus(Some(next_window))?;
-
- if self.layout.name() == "scrolling" {
- self.scroll_to_window(next_window, true)?;
- }
-
- self.restack()?;
- self.update_tab_bars()?;
-
- Ok(())
- }
-
- pub fn move_stack(&mut self, direction: i32) -> WmResult<()> {
- let monitor_index = self.selected_monitor;
- let monitor = match self.monitors.get(monitor_index) {
- Some(m) => m.clone(),
- None => return Ok(()),
- };
-
- let selected = match monitor.selected_client {
- Some(win) => win,
- None => return Ok(()),
- };
-
- let selected_client = match self.clients.get(&selected) {
- Some(c) => c,
- None => return Ok(()),
- };
-
- let target = if direction > 0 {
- let next = self.next_tiled(selected_client.next, &monitor);
- if next.is_some() {
- next
- } else {
- self.next_tiled(monitor.clients_head, &monitor)
- }
- } else {
- let mut previous = None;
- let mut current = monitor.clients_head;
- while let Some(window) = current {
- if window == selected {
- break;
- }
- if let Some(client) = self.clients.get(&window) {
- let visible_tags = client.tags & monitor.tagset[monitor.selected_tags_index];
- if visible_tags != 0 && !client.is_floating {
- previous = Some(window);
- }
- current = client.next;
- } else {
- break;
- }
- }
- if previous.is_none() {
- let mut last = None;
- let mut current = monitor.clients_head;
- while let Some(window) = current {
- if let Some(client) = self.clients.get(&window) {
- let visible_tags =
- client.tags & monitor.tagset[monitor.selected_tags_index];
- if visible_tags != 0 && !client.is_floating {
- last = Some(window);
- }
- current = client.next;
- } else {
- break;
- }
- }
- last
- } else {
- previous
- }
- };
-
- let target = match target {
- Some(t) if t != selected => t,
- _ => return Ok(()),
- };
-
- let mut prev_selected = None;
- let mut prev_target = None;
- let mut current = monitor.clients_head;
-
- while let Some(window) = current {
- if let Some(client) = self.clients.get(&window) {
- if client.next == Some(selected) {
- prev_selected = Some(window);
- }
- if client.next == Some(target) {
- prev_target = Some(window);
- }
- current = client.next;
- } else {
- break;
- }
- }
-
- let selected_next = self.clients.get(&selected).and_then(|c| c.next);
- let target_next = self.clients.get(&target).and_then(|c| c.next);
-
- let temp = if selected_next == Some(target) {
- Some(selected)
- } else {
- selected_next
- };
-
- if let Some(client) = self.clients.get_mut(&selected) {
- client.next = if target_next == Some(selected) {
- Some(target)
- } else {
- target_next
- };
- }
-
- if let Some(client) = self.clients.get_mut(&target) {
- client.next = temp;
- }
-
- if let Some(prev) = prev_selected
- && prev != target
- && let Some(client) = self.clients.get_mut(&prev)
- {
- client.next = Some(target);
- }
-
- if let Some(prev) = prev_target
- && prev != selected
- && let Some(client) = self.clients.get_mut(&prev)
- {
- client.next = Some(selected);
- }
-
- if let Some(monitor) = self.monitors.get_mut(monitor_index) {
- if monitor.clients_head == Some(selected) {
- monitor.clients_head = Some(target);
- } else if monitor.clients_head == Some(target) {
- monitor.clients_head = Some(selected);
- }
- }
-
- self.apply_layout()?;
- Ok(())
- }
-
- pub fn focus_monitor(&mut self, direction: i32) -> WmResult<()> {
- if self.monitors.len() <= 1 {
- return Ok(());
- }
-
- let target_monitor = match self.get_adjacent_monitor(direction) {
- Some(idx) if idx != self.selected_monitor => idx,
- _ => return Ok(()),
- };
-
- let old_selected = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- if let Some(win) = old_selected {
- self.unfocus(win, true)?;
- }
-
- self.selected_monitor = target_monitor;
- self.focus(None)?;
-
- Ok(())
- }
-
- pub fn send_window_to_adjacent_monitor(&mut self, direction: i32) -> WmResult<()> {
- if self.monitors.len() <= 1 {
- return Ok(());
- }
-
- let selected_window = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- let window = match selected_window {
- Some(win) => win,
- None => return Ok(()),
- };
-
- let target_monitor = match self.get_adjacent_monitor(direction) {
- Some(idx) => idx,
- None => return Ok(()),
- };
-
- self.move_window_to_monitor(window, target_monitor)?;
-
- Ok(())
- }
-
- fn drag_window(&mut self, window: Window) -> WmResult<()> {
- let is_fullscreen = self
- .clients
- .get(&window)
- .map(|c| c.is_fullscreen)
- .unwrap_or(false);
-
- if is_fullscreen {
- return Ok(());
- }
-
- let client_info = self.clients.get(&window).map(|c| {
- (
- c.x_position,
- c.y_position,
- c.width,
- c.height,
- c.is_floating,
- c.monitor_index,
- )
- });
-
- let Some((orig_x, orig_y, width, height, was_floating, monitor_idx)) = client_info else {
- return Ok(());
- };
-
- let monitor = self.monitors.get(monitor_idx).cloned();
- let Some(monitor) = monitor else {
- return Ok(());
- };
-
- let snap = 32;
- let is_normie = self.layout.name() == "normie";
-
- if !was_floating && !is_normie {
- self.toggle_floating()?;
- }
-
- self.connection
- .grab_pointer(
- false,
- self.root,
- EventMask::POINTER_MOTION | EventMask::BUTTON_RELEASE | EventMask::BUTTON_PRESS,
- GrabMode::ASYNC,
- GrabMode::ASYNC,
- x11rb::NONE,
- x11rb::NONE,
- x11rb::CURRENT_TIME,
- )?
- .reply()?;
-
- let pointer = self.connection.query_pointer(self.root)?.reply()?;
- let (start_x, start_y) = (pointer.root_x as i32, pointer.root_y as i32);
-
- let mut last_time = 0u32;
-
- loop {
- let event = self.connection.wait_for_event()?;
- match event {
- Event::ConfigureRequest(_) | Event::MapRequest(_) | Event::Expose(_) => {}
- Event::MotionNotify(e) => {
- if e.time.wrapping_sub(last_time) <= 16 {
- continue;
- }
- last_time = e.time;
-
- let mut new_x = orig_x as i32 + (e.root_x as i32 - start_x);
- let mut new_y = orig_y as i32 + (e.root_y as i32 - start_y);
-
- if (monitor.window_area_x - new_x).abs() < snap {
- new_x = monitor.window_area_x;
- } else if ((monitor.window_area_x + monitor.window_area_width)
- - (new_x + width as i32))
- .abs()
- < snap
- {
- new_x = monitor.window_area_x + monitor.window_area_width - width as i32;
- }
-
- if (monitor.window_area_y - new_y).abs() < snap {
- new_y = monitor.window_area_y;
- } else if ((monitor.window_area_y + monitor.window_area_height)
- - (new_y + height as i32))
- .abs()
- < snap
- {
- new_y = monitor.window_area_y + monitor.window_area_height - height as i32;
- }
-
- let should_resize = is_normie
- || self
- .clients
- .get(&window)
- .map(|c| c.is_floating)
- .unwrap_or(false);
-
- if should_resize {
- if let Some(client) = self.clients.get_mut(&window) {
- client.x_position = new_x as i16;
- client.y_position = new_y as i16;
- }
-
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new().x(new_x).y(new_y),
- )?;
- self.connection.flush()?;
- }
- }
- Event::ButtonRelease(_) => break,
- _ => {}
- }
- }
-
- self.connection
- .ungrab_pointer(x11rb::CURRENT_TIME)?
- .check()?;
-
- let final_client = self
- .clients
- .get(&window)
- .map(|c| (c.x_position, c.y_position, c.width, c.height));
-
- if let Some((x, y, w, h)) = final_client {
- let new_monitor = self.get_monitor_for_rect(x as i32, y as i32, w as i32, h as i32);
- if new_monitor != monitor_idx {
- self.move_window_to_monitor(window, new_monitor)?;
- self.selected_monitor = new_monitor;
- self.focus(None)?;
- }
- }
-
- if self.config.auto_tile && !was_floating && !is_normie {
- let drop_monitor_idx = self
- .clients
- .get(&window)
- .map(|c| c.monitor_index)
- .unwrap_or(monitor_idx);
-
- if let Some((x, y, w, h)) = final_client {
- let center = (x as i32 + w as i32 / 2, y as i32 + h as i32 / 2);
- if let Some(target) = self.tiled_window_at(window, drop_monitor_idx, center) {
- self.detach(window);
- self.insert_before(window, target, drop_monitor_idx);
- }
- }
-
- self.floating_windows.remove(&window);
- if let Some(client) = self.clients.get_mut(&window) {
- client.is_floating = false;
- }
- self.apply_layout()?;
- }
-
- Ok(())
- }
-
- fn tiled_window_at(
- &self,
- exclude: Window,
- monitor_idx: usize,
- (px, py): (i32, i32),
- ) -> Option<Window> {
- let monitor = self.monitors.get(monitor_idx)?;
- let tags = monitor.tagset[monitor.selected_tags_index];
- let mut current = monitor.clients_head;
-
- while let Some(win) = current {
- let c = self.clients.get(&win)?;
- current = c.next;
-
- if win == exclude || c.is_floating || (c.tags & tags) == 0 {
- continue;
- }
-
- let (x, y) = (c.x_position as i32, c.y_position as i32);
- let (w, h) = (
- c.width as i32 + c.border_width as i32 * 2,
- c.height as i32 + c.border_width as i32 * 2,
- );
-
- if px >= x && px < x + w && py >= y && py < y + h {
- return Some(win);
- }
- }
- None
- }
-
- fn insert_before(&mut self, window: Window, target: Window, monitor_idx: usize) {
- let Some(monitor) = self.monitors.get_mut(monitor_idx) else {
- return;
- };
-
- if monitor.clients_head == Some(target) {
- if let Some(c) = self.clients.get_mut(&window) {
- c.next = Some(target);
- }
- monitor.clients_head = Some(window);
- return;
- }
-
- let mut current = monitor.clients_head;
- while let Some(w) = current {
- let Some(c) = self.clients.get(&w) else { break };
- if c.next != Some(target) {
- current = c.next;
- continue;
- }
- if let Some(prev) = self.clients.get_mut(&w) {
- prev.next = Some(window);
- }
- if let Some(inserted) = self.clients.get_mut(&window) {
- inserted.next = Some(target);
- }
- break;
- }
- }
-
- fn resize_window_with_mouse(&mut self, window: Window) -> WmResult<()> {
- let is_fullscreen = self
- .clients
- .get(&window)
- .map(|c| c.is_fullscreen)
- .unwrap_or(false);
-
- if is_fullscreen {
- return Ok(());
- }
-
- let client_info = self.clients.get(&window).map(|c| {
- (
- c.x_position,
- c.y_position,
- c.width,
- c.height,
- c.border_width,
- c.is_floating,
- c.monitor_index,
- )
- });
-
- let Some((
- orig_x,
- orig_y,
- orig_width,
- orig_height,
- border_width,
- was_floating,
- monitor_idx,
- )) = client_info
- else {
- return Ok(());
- };
-
- let monitor = match self.monitors.get(monitor_idx) {
- Some(m) => m,
- None => return Ok(()),
- };
-
- let is_normie = self.layout.name() == "normie";
-
- if self.config.auto_tile && !was_floating && !is_normie {
- let mut tiled_count = 0;
- let mut current = monitor.clients_head;
- while let Some(w) = current {
- if let Some(c) = self.clients.get(&w) {
- let visible = (c.tags & monitor.tagset[monitor.selected_tags_index]) != 0;
- if visible && !c.is_floating {
- tiled_count += 1;
- }
- current = c.next;
- } else {
- break;
- }
- }
- if tiled_count <= 1 {
- return Ok(());
- }
- }
-
- if !was_floating && !is_normie {
- self.toggle_floating()?;
- }
-
- self.connection.warp_pointer(
- x11rb::NONE,
- window,
- 0,
- 0,
- 0,
- 0,
- (orig_width + border_width - 1) as i16,
- (orig_height + border_width - 1) as i16,
- )?;
-
- self.connection
- .grab_pointer(
- false,
- self.root,
- EventMask::POINTER_MOTION | EventMask::BUTTON_RELEASE | EventMask::BUTTON_PRESS,
- GrabMode::ASYNC,
- GrabMode::ASYNC,
- x11rb::NONE,
- x11rb::NONE,
- x11rb::CURRENT_TIME,
- )?
- .reply()?;
-
- let mut last_time = 0u32;
-
- loop {
- let event = self.connection.wait_for_event()?;
- match event {
- Event::ConfigureRequest(_) | Event::MapRequest(_) | Event::Expose(_) => {}
- Event::MotionNotify(e) => {
- if e.time.wrapping_sub(last_time) <= 16 {
- continue;
- }
- last_time = e.time;
-
- let new_width = ((e.root_x as i32 - orig_x as i32 - 2 * border_width as i32
- + 1)
- .max(1)) as u32;
- let new_height = ((e.root_y as i32 - orig_y as i32 - 2 * border_width as i32
- + 1)
- .max(1)) as u32;
-
- let should_resize = is_normie
- || self
- .clients
- .get(&window)
- .map(|c| c.is_floating)
- .unwrap_or(false);
-
- if should_resize && let Some(client) = self.clients.get(&window).cloned() {
- let (_, _, hint_width, hint_height, _) = self.apply_size_hints(
- window,
- client.x_position as i32,
- client.y_position as i32,
- new_width as i32,
- new_height as i32,
- );
-
- if let Some(client_mut) = self.clients.get_mut(&window) {
- client_mut.width = hint_width as u16;
- client_mut.height = hint_height as u16;
- }
-
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .width(hint_width as u32)
- .height(hint_height as u32),
- )?;
- self.connection.flush()?;
- }
- }
- Event::ButtonRelease(_) => break,
- _ => {}
- }
- }
-
- let final_client = self.clients.get(&window).map(|c| (c.width, c.border_width));
-
- if let Some((w, bw)) = final_client {
- self.connection.warp_pointer(
- x11rb::NONE,
- window,
- 0,
- 0,
- 0,
- 0,
- (w + bw - 1) as i16,
- (w + bw - 1) as i16,
- )?;
- }
-
- self.connection
- .ungrab_pointer(x11rb::CURRENT_TIME)?
- .check()?;
-
- let final_client_pos = self
- .clients
- .get(&window)
- .map(|c| (c.x_position, c.y_position, c.width, c.height));
-
- if let Some((x, y, w, h)) = final_client_pos {
- let new_monitor = self.get_monitor_for_rect(x as i32, y as i32, w as i32, h as i32);
- if new_monitor != monitor_idx {
- self.move_window_to_monitor(window, new_monitor)?;
- self.selected_monitor = new_monitor;
- self.focus(None)?;
- }
- }
-
- if self.config.auto_tile && !was_floating && !is_normie {
- self.floating_windows.remove(&window);
- if let Some(client) = self.clients.get_mut(&window) {
- client.is_floating = false;
- }
- self.apply_layout()?;
- }
-
- Ok(())
- }
-
- fn handle_event(&mut self, event: Event) -> WmResult<Control> {
- match event {
- Event::KeyPress(ref key_event) if key_event.event == self.overlay.window() => {
- if self.overlay.is_visible()
- && let Err(error) = self.overlay.hide(&self.connection)
- {
- eprintln!("Failed to hide overlay: {:?}", error);
- }
- return Ok(Control::Continue);
- }
- Event::ButtonPress(ref button_event) if button_event.event == self.overlay.window() => {
- if self.overlay.is_visible()
- && let Err(error) = self.overlay.hide(&self.connection)
- {
- eprintln!("Failed to hide overlay: {:?}", error);
- }
- return Ok(Control::Continue);
- }
- Event::Expose(ref expose_event) if expose_event.window == self.overlay.window() => {
- if self.overlay.is_visible()
- && let Err(error) = self.overlay.draw(&self.connection, &self.font)
- {
- eprintln!("Failed to draw overlay: {:?}", error);
- }
- return Ok(Control::Continue);
- }
- Event::KeyPress(ref e) if e.event == self.keybind_overlay.window() => {
- if self.keybind_overlay.is_visible()
- && !self.keybind_overlay.should_suppress_input()
- {
- use crate::keyboard::keysyms;
- if let Some(mapping) = &self.keyboard_mapping {
- let keysym = mapping.keycode_to_keysym(e.detail);
- let is_escape = keysym == keysyms::XK_ESCAPE;
- let is_q = keysym == keysyms::XK_Q || keysym == 0x0051;
- if (is_escape || is_q)
- && let Err(error) = self.keybind_overlay.hide(&self.connection)
- {
- eprintln!("Failed to hide keybind overlay: {:?}", error);
- }
- }
- }
- return Ok(Control::Continue);
- }
- Event::ButtonPress(ref e) if e.event == self.keybind_overlay.window() => {
- self.connection
- .allow_events(Allow::REPLAY_POINTER, e.time)?;
- return Ok(Control::Continue);
- }
- Event::Expose(ref expose_event)
- if expose_event.window == self.keybind_overlay.window() =>
- {
- if self.keybind_overlay.is_visible()
- && let Err(error) = self.keybind_overlay.draw(&self.connection, &self.font)
- {
- eprintln!("Failed to draw keybind overlay: {:?}", error);
- }
- return Ok(Control::Continue);
- }
- Event::MapRequest(event) => {
- let attrs = match self.connection.get_window_attributes(event.window)?.reply() {
- Ok(attrs) => attrs,
- Err(_) => return Ok(Control::Continue),
- };
-
- if attrs.override_redirect {
- return Ok(Control::Continue);
- }
-
- if !self.windows.contains(&event.window) {
- self.manage_window(event.window)?;
- }
- }
- Event::UnmapNotify(event) => {
- if self.windows.contains(&event.window) && self.is_window_visible(event.window) {
- self.remove_window(event.window, false)?;
- }
- }
- Event::DestroyNotify(event) => {
- if self.windows.contains(&event.window) {
- self.remove_window(event.window, true)?;
- }
- }
- Event::PropertyNotify(event) => {
- if event.state == Property::DELETE {
- return Ok(Control::Continue);
- }
-
- if !self.clients.contains_key(&event.window) {
- return Ok(Control::Continue);
- }
-
- if event.atom == AtomEnum::WM_TRANSIENT_FOR.into() {
- let is_floating = self
- .clients
- .get(&event.window)
- .map(|c| c.is_floating)
- .unwrap_or(false);
- if !is_floating
- && let Some(parent) = self.get_transient_parent(event.window)
- && self.clients.contains_key(&parent)
- {
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.is_floating = true;
- }
- self.floating_windows.insert(event.window);
- self.apply_layout()?;
- }
- } else if event.atom == AtomEnum::WM_NORMAL_HINTS.into() {
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.hints_valid = false;
- }
- } else if event.atom == AtomEnum::WM_HINTS.into() {
- self.update_window_hints(event.window)?;
- self.update_bar()?;
- }
-
- if event.atom == self.atoms.wm_name || event.atom == self.atoms.net_wm_name {
- let _ = self.update_window_title(event.window);
- if self.layout.name() == "tabbed" {
- self.update_tab_bars()?;
- }
- }
-
- if event.atom == self.atoms.net_wm_window_type {
- self.update_window_type(event.window)?;
- }
- }
- Event::EnterNotify(event) => {
- if event.mode != x11rb::protocol::xproto::NotifyMode::NORMAL
- || event.detail == x11rb::protocol::xproto::NotifyDetail::INFERIOR
- {
- return Ok(Control::Continue);
- }
- if self.windows.contains(&event.event) {
- if let Some(client) = self.clients.get(&event.event)
- && client.monitor_index != self.selected_monitor
- {
- if let Some(old_selected) = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|monitor| monitor.selected_client)
- {
- self.unfocus(old_selected, false)?;
- }
- self.selected_monitor = client.monitor_index;
- self.update_bar()?;
- }
- self.focus(Some(event.event))?;
- self.update_tab_bars()?;
- }
- }
- Event::MotionNotify(event) => {
- if event.event != self.root {
- return Ok(Control::Continue);
- }
-
- if let Some(monitor_index) =
- self.get_monitor_at_point(event.root_x as i32, event.root_y as i32)
- && monitor_index != self.selected_monitor
- {
- if let Some(old_selected) = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|monitor| monitor.selected_client)
- {
- self.unfocus(old_selected, true)?;
- }
-
- self.selected_monitor = monitor_index;
- self.focus(None)?;
- self.update_bar()?;
- self.update_tab_bars()?;
- }
- }
- Event::KeyPress(event) => {
- let Some(mapping) = &self.keyboard_mapping else {
- return Ok(Control::Continue);
- };
-
- let result = keyboard::handle_key_press(
- event,
- &self.config.keybindings,
- &self.keychord_state,
- mapping,
- );
-
- match result {
- keyboard::handlers::KeychordResult::Completed(action, arg) => {
- self.keychord_state = keyboard::handlers::KeychordState::Idle;
- self.current_key = 0;
- self.grab_keys()?;
- self.update_bar()?;
-
- match action {
- KeyAction::Quit => return Ok(Control::Quit),
- KeyAction::Restart => match self.try_reload_config() {
- Ok(()) => {
- self.gaps_enabled = self.config.gaps_enabled;
- self.error_message = None;
- if let Err(error) = self.overlay.hide(&self.connection) {
- eprintln!(
- "Failed to hide overlay after config reload: {:?}",
- error
- );
- }
- self.apply_layout()?;
- self.update_bar()?;
- }
- Err(err) => {
- eprintln!("Config reload error: {}", err);
- self.error_message = Some(err.to_string());
- let monitor = &self.monitors[self.selected_monitor];
- let monitor_x = monitor.screen_x as i16;
- let monitor_y = monitor.screen_y as i16;
- let screen_width = monitor.screen_width as u16;
- let screen_height = monitor.screen_height as u16;
- match self.overlay.show_error(
- &self.connection,
- &self.font,
- err,
- monitor_x,
- monitor_y,
- screen_width,
- screen_height,
- ) {
- Ok(()) => eprintln!("Error modal displayed"),
- Err(e) => eprintln!("Failed to show error modal: {:?}", e),
- }
- }
- },
- _ => self.handle_key_action(action, &arg)?,
- }
- }
- keyboard::handlers::KeychordResult::InProgress(candidates) => {
- self.current_key += 1;
- self.keychord_state = keyboard::handlers::KeychordState::InProgress {
- candidates: candidates.clone(),
- keys_pressed: self.current_key,
- };
- self.grab_keys()?;
- self.update_bar()?;
- }
- keyboard::handlers::KeychordResult::Cancelled
- | keyboard::handlers::KeychordResult::None => {
- self.keychord_state = keyboard::handlers::KeychordState::Idle;
- self.current_key = 0;
- self.grab_keys()?;
- self.update_bar()?;
- }
- }
- }
- Event::ButtonPress(event) => {
- if self.keybind_overlay.is_visible()
- && event.event != self.keybind_overlay.window()
- && let Err(error) = self.keybind_overlay.hide(&self.connection)
- {
- eprintln!("Failed to hide keybind overlay: {:?}", error);
- }
-
- let is_bar_click = self
- .bars
- .iter()
- .enumerate()
- .find(|(_, bar)| bar.window() == event.event);
-
- if let Some((monitor_index, bar)) = is_bar_click {
- if let Some(tag_index) = bar.handle_click(event.event_x) {
- if monitor_index != self.selected_monitor {
- self.selected_monitor = monitor_index;
- }
- self.view_tag(tag_index)?;
- }
- } else {
- let is_tab_bar_click = self
- .tab_bars
- .iter()
- .enumerate()
- .find(|(_, tab_bar)| tab_bar.window() == event.event);
-
- if let Some((monitor_index, tab_bar)) = is_tab_bar_click {
- if monitor_index != self.selected_monitor {
- self.selected_monitor = monitor_index;
- }
-
- let visible_windows: Vec<(Window, String)> = self
- .windows
- .iter()
- .filter_map(|&window| {
- if let Some(client) = self.clients.get(&window) {
- if client.monitor_index != monitor_index
- || self.floating_windows.contains(&window)
- || self.fullscreen_windows.contains(&window)
- {
- return None;
- }
- let monitor_tags = self
- .monitors
- .get(monitor_index)
- .map(|m| m.tagset[m.selected_tags_index])
- .unwrap_or(0);
- if (client.tags & monitor_tags) != 0 {
- return Some((window, client.name.clone()));
- }
- }
- None
- })
- .collect();
-
- if let Some(clicked_window) =
- tab_bar.get_clicked_window(&visible_windows, event.event_x)
- {
- self.connection.configure_window(
- clicked_window,
- &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
- )?;
- self.focus(Some(clicked_window))?;
- self.update_tab_bars()?;
- }
- } else if event.child != x11rb::NONE {
- self.focus(Some(event.child))?;
- self.restack()?;
- self.update_tab_bars()?;
-
- let state_clean = u16::from(event.state)
- & !(u16::from(ModMask::LOCK) | u16::from(ModMask::M2));
- let modkey_held = state_clean & u16::from(self.config.modkey) != 0;
-
- if modkey_held && event.detail == ButtonIndex::M1.into() {
- if self.clients.contains_key(&event.child) {
- self.drag_window(event.child)?;
- }
- self.connection
- .allow_events(Allow::REPLAY_POINTER, event.time)?;
- } else if modkey_held && event.detail == ButtonIndex::M3.into() {
- if self.clients.contains_key(&event.child) {
- self.resize_window_with_mouse(event.child)?;
- }
- self.connection
- .allow_events(Allow::REPLAY_POINTER, event.time)?;
- } else {
- self.connection
- .allow_events(Allow::REPLAY_POINTER, event.time)?;
- }
- } else if self.windows.contains(&event.event) {
- self.focus(Some(event.event))?;
- self.restack()?;
- self.update_tab_bars()?;
-
- let state_clean = u16::from(event.state)
- & !(u16::from(ModMask::LOCK) | u16::from(ModMask::M2));
- let modkey_held = state_clean & u16::from(self.config.modkey) != 0;
-
- if modkey_held && event.detail == ButtonIndex::M1.into() {
- self.drag_window(event.event)?;
- self.connection
- .allow_events(Allow::REPLAY_POINTER, event.time)?;
- } else if modkey_held && event.detail == ButtonIndex::M3.into() {
- self.resize_window_with_mouse(event.event)?;
- self.connection
- .allow_events(Allow::REPLAY_POINTER, event.time)?;
- } else {
- self.connection
- .allow_events(Allow::REPLAY_POINTER, event.time)?;
- }
- } else {
- self.connection
- .allow_events(Allow::REPLAY_POINTER, event.time)?;
- }
- }
- }
- Event::Expose(event) => {
- for bar in &mut self.bars {
- if event.window == bar.window() {
- bar.invalidate();
- self.update_bar()?;
- break;
- }
- }
- for _tab_bar in &self.tab_bars {
- if event.window == _tab_bar.window() {
- self.update_tab_bars()?;
- break;
- }
- }
- }
- Event::ConfigureRequest(event) => {
- if let Some(client) = self.clients.get(&event.window) {
- let monitor = &self.monitors[client.monitor_index];
- let is_floating = client.is_floating;
- let is_fullscreen = client.is_fullscreen;
- let has_layout = self.layout.name() != "normie";
-
- if event.value_mask.contains(ConfigWindow::BORDER_WIDTH) {
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.border_width = event.border_width;
- }
- } else if is_fullscreen {
- self.send_configure_notify(event.window)?;
- } else if is_floating || !has_layout {
- let mut x = client.x_position as i32;
- let mut y = client.y_position as i32;
- let mut w = client.width as i32;
- let mut h = client.height as i32;
-
- if event.value_mask.contains(ConfigWindow::X) {
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.old_x_position = c.x_position;
- }
- x = monitor.screen_x + event.x as i32;
- }
- if event.value_mask.contains(ConfigWindow::Y) {
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.old_y_position = c.y_position;
- }
- y = monitor.screen_y + event.y as i32;
- }
- if event.value_mask.contains(ConfigWindow::WIDTH) {
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.old_width = c.width;
- }
- w = event.width as i32;
- }
- if event.value_mask.contains(ConfigWindow::HEIGHT) {
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.old_height = c.height;
- }
- h = event.height as i32;
- }
-
- let bw = self.config.border_width as i32;
- let width_with_border = w + 2 * bw;
- let height_with_border = h + 2 * bw;
-
- if (x + w) > monitor.screen_x + monitor.screen_width && is_floating {
- x = monitor.screen_x
- + (monitor.screen_width / 2 - width_with_border / 2);
- }
- if (y + h) > monitor.screen_y + monitor.screen_height && is_floating {
- y = monitor.screen_y
- + (monitor.screen_height / 2 - height_with_border / 2);
- }
-
- if let Some(c) = self.clients.get_mut(&event.window) {
- c.x_position = x as i16;
- c.y_position = y as i16;
- c.width = w as u16;
- c.height = h as u16;
- }
-
- let only_position_change = event.value_mask.contains(ConfigWindow::X)
- || event.value_mask.contains(ConfigWindow::Y);
- let no_size_change = !event.value_mask.contains(ConfigWindow::WIDTH)
- && !event.value_mask.contains(ConfigWindow::HEIGHT);
- if only_position_change && no_size_change {
- self.send_configure_notify(event.window)?;
- }
-
- if self.is_visible(event.window) {
- self.connection.configure_window(
- event.window,
- &ConfigureWindowAux::new()
- .x(x)
- .y(y)
- .width(w as u32)
- .height(h as u32),
- )?;
- }
- } else {
- self.send_configure_notify(event.window)?;
- }
- } else {
- let mut aux = ConfigureWindowAux::new();
- if event.value_mask.contains(ConfigWindow::X) {
- aux = aux.x(event.x as i32);
- }
- if event.value_mask.contains(ConfigWindow::Y) {
- aux = aux.y(event.y as i32);
- }
- if event.value_mask.contains(ConfigWindow::WIDTH) {
- aux = aux.width(event.width as u32);
- }
- if event.value_mask.contains(ConfigWindow::HEIGHT) {
- aux = aux.height(event.height as u32);
- }
- if event.value_mask.contains(ConfigWindow::BORDER_WIDTH) {
- aux = aux.border_width(event.border_width as u32);
- }
- if event.value_mask.contains(ConfigWindow::SIBLING) {
- aux = aux.sibling(event.sibling);
- }
- if event.value_mask.contains(ConfigWindow::STACK_MODE) {
- aux = aux.stack_mode(event.stack_mode);
- }
- self.connection.configure_window(event.window, &aux)?;
- }
- self.connection.flush()?;
- }
- Event::ClientMessage(event) => {
- if !self.clients.contains_key(&event.window) {
- return Ok(Control::Continue);
- }
-
- if event.type_ == self.atoms.net_wm_state {
- let data = event.data.as_data32();
- let atom1 = data.get(1).copied().unwrap_or(0);
- let atom2 = data.get(2).copied().unwrap_or(0);
-
- if atom1 == self.atoms.net_wm_state_fullscreen
- || atom2 == self.atoms.net_wm_state_fullscreen
- {
- let action = data[0];
- let fullscreen = match action {
- 1 => true,
- 0 => false,
- 2 => !self.fullscreen_windows.contains(&event.window),
- _ => return Ok(Control::Continue),
- };
- self.set_window_fullscreen(event.window, fullscreen)?;
- self.restack()?;
- }
- } else if event.type_ == self.atoms.net_active_window {
- let selected_window = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- let is_urgent = self
- .clients
- .get(&event.window)
- .map(|c| c.is_urgent)
- .unwrap_or(false);
-
- if Some(event.window) != selected_window && !is_urgent {
- self.set_urgent(event.window, true)?;
- }
- }
- }
- Event::FocusIn(event) => {
- if !self.windows.contains(&event.event) {
- return Ok(Control::Continue);
- }
-
- let event_window_visible = self.is_visible(event.event);
-
- if !event_window_visible {
- self.focus(None)?;
- } else {
- let selected_window = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|monitor| monitor.selected_client);
-
- if let Some(focused_window) = selected_window
- && event.event != focused_window
- {
- self.set_focus(focused_window)?;
- }
- }
- }
- Event::MappingNotify(event) => {
- if event.request == x11rb::protocol::xproto::Mapping::KEYBOARD {
- self.grab_keys()?;
- }
- }
- Event::ConfigureNotify(event) => {
- if event.window == self.root {
- let old_width = self.screen.width_in_pixels;
- let old_height = self.screen.height_in_pixels;
-
- if event.width != old_width || event.height != old_height {
- self.screen = self.connection.setup().roots[self.screen_number].clone();
-
- for monitor_index in 0..self.monitors.len() {
- let monitor = &self.monitors[monitor_index];
- let monitor_x = monitor.screen_x;
- let monitor_y = monitor.screen_y;
- let monitor_width = monitor.screen_width as u32;
- let monitor_height = monitor.screen_height as u32;
-
- let fullscreen_on_monitor: Vec<Window> = self
- .fullscreen_windows
- .iter()
- .filter(|&&window| {
- self.clients
- .get(&window)
- .map(|client| client.monitor_index == monitor_index)
- .unwrap_or(false)
- })
- .copied()
- .collect();
-
- for window in fullscreen_on_monitor {
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .x(monitor_x)
- .y(monitor_y)
- .width(monitor_width)
- .height(monitor_height),
- )?;
- }
- }
-
- self.apply_layout()?;
- }
- }
- }
- _ => {}
- }
- Ok(Control::Continue)
- }
-
- fn apply_layout(&mut self) -> WmResult<()> {
- for monitor_index in 0..self.monitors.len() {
- let stack_head = self.monitors.get(monitor_index).and_then(|m| m.stack_head);
- self.showhide(stack_head)?;
- }
-
- let is_normie = self.layout.name() == LayoutType::Normie.as_str();
-
- if !is_normie {
- let monitor_count = self.monitors.len();
- for monitor_index in 0..monitor_count {
- let monitor = &self.monitors[monitor_index];
- let border_width = self.config.border_width;
-
- let gaps = if self.gaps_enabled {
- GapConfig {
- inner_horizontal: self.config.gap_inner_horizontal,
- inner_vertical: self.config.gap_inner_vertical,
- outer_horizontal: self.config.gap_outer_horizontal,
- outer_vertical: self.config.gap_outer_vertical,
- }
- } else {
- GapConfig {
- inner_horizontal: 0,
- inner_vertical: 0,
- outer_horizontal: 0,
- outer_vertical: 0,
- }
- };
-
- let monitor_x = monitor.screen_x;
- let monitor_y = monitor.screen_y;
- let monitor_width = monitor.screen_width;
- let monitor_height = monitor.screen_height;
- let scroll_offset = monitor.scroll_offset;
-
- let mut visible: Vec<Window> = Vec::new();
- let mut current = self.next_tiled(monitor.clients_head, monitor);
- while let Some(window) = current {
- visible.push(window);
- if let Some(client) = self.clients.get(&window) {
- current = self.next_tiled(client.next, monitor);
- } else {
- break;
- }
- }
-
- let bar_height = if self.show_bar {
- self.bars
- .get(monitor_index)
- .map(|bar| bar.height() as u32)
- .unwrap_or(0)
- } else {
- 0
- };
- let usable_height = monitor_height.saturating_sub(bar_height as i32);
- let master_factor = monitor.master_factor;
- let num_master = monitor.num_master;
- let smartgaps_enabled = self.config.smartgaps_enabled;
-
- let geometries = self.layout.arrange(
- &visible,
- monitor_width as u32,
- usable_height as u32,
- &gaps,
- master_factor,
- num_master,
- smartgaps_enabled,
- );
-
- for (window, geometry) in visible.iter().zip(geometries.iter()) {
- let mut adjusted_width = geometry.width.saturating_sub(2 * border_width);
- let mut adjusted_height = geometry.height.saturating_sub(2 * border_width);
-
- if let Some(client) = self.clients.get(window).cloned()
- && !client.is_floating
- {
- let (_, _, hint_width, hint_height, _) = self.apply_size_hints(
- *window,
- geometry.x_coordinate,
- geometry.y_coordinate,
- adjusted_width as i32,
- adjusted_height as i32,
- );
- adjusted_width = hint_width as u32;
- adjusted_height = hint_height as u32;
- }
-
- let is_scrolling = self.layout.name() == "scrolling";
- let adjusted_x = if is_scrolling {
- geometry.x_coordinate + monitor_x - scroll_offset
- } else {
- geometry.x_coordinate + monitor_x
- };
- let adjusted_y = geometry.y_coordinate + monitor_y + bar_height as i32;
-
- if let Some(client) = self.clients.get_mut(window) {
- client.x_position = adjusted_x as i16;
- client.y_position = adjusted_y as i16;
- client.width = adjusted_width as u16;
- client.height = adjusted_height as u16;
- }
-
- self.connection.configure_window(
- *window,
- &ConfigureWindowAux::new()
- .x(adjusted_x)
- .y(adjusted_y)
- .width(adjusted_width)
- .height(adjusted_height)
- .border_width(border_width),
- )?;
-
- if let Some(c) = self.clients.get_mut(window) {
- c.x_position = adjusted_x as i16;
- c.y_position = adjusted_y as i16;
- c.width = adjusted_width as u16;
- c.height = adjusted_height as u16;
- c.border_width = border_width as u16;
- }
- }
- }
- }
-
- for monitor_index in 0..self.monitors.len() {
- let stack_head = self.monitors[monitor_index].stack_head;
- self.showhide(stack_head)?;
- }
-
- for monitor_index in 0..self.monitors.len() {
- let monitor = &self.monitors[monitor_index];
- let tags = monitor.tagset[monitor.selected_tags_index];
-
- let has_visible_fullscreen = self.fullscreen_windows.iter().any(|&w| {
- self.clients.get(&w).map_or(false, |c| {
- c.monitor_index == monitor_index && (c.tags & tags) != 0
- })
- });
-
- if has_visible_fullscreen {
- if let Some(bar) = self.bars.get(monitor_index) {
- self.connection.unmap_window(bar.window())?;
- }
-
- for &window in &self.fullscreen_windows {
- if let Some(client) = self.clients.get(&window) {
- if client.monitor_index == monitor_index && (client.tags & tags) != 0 {
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new()
- .border_width(0)
- .x(monitor.screen_x)
- .y(monitor.screen_y)
- .width(monitor.screen_width as u32)
- .height(monitor.screen_height as u32)
- .stack_mode(StackMode::ABOVE),
- )?;
- }
- }
- }
- } else if self.show_bar {
- if let Some(bar) = self.bars.get(monitor_index) {
- self.connection.map_window(bar.window())?;
- }
- }
- }
-
- self.connection.flush()?;
-
- let is_tabbed = self.layout.name() == LayoutType::Tabbed.as_str();
-
- if is_tabbed {
- let outer_horizontal = if self.gaps_enabled {
- self.config.gap_outer_horizontal
- } else {
- 0
- };
- let outer_vertical = if self.gaps_enabled {
- self.config.gap_outer_vertical
- } else {
- 0
- };
-
- for monitor_index in 0..self.tab_bars.len() {
- if let Some(monitor) = self.monitors.get(monitor_index) {
- let bar_height = if self.show_bar {
- self.bars
- .get(monitor_index)
- .map(|bar| bar.height() as f32)
- .unwrap_or(0.0)
- } else {
- 0.0
- };
-
- let tab_bar_x = (monitor.screen_x + outer_horizontal as i32) as i16;
- let tab_bar_y =
- (monitor.screen_y as f32 + bar_height + outer_vertical as f32) as i16;
- let tab_bar_width = monitor
- .screen_width
- .saturating_sub(2 * outer_horizontal as i32)
- as u16;
-
- if let Err(e) = self.tab_bars[monitor_index].reposition(
- &self.connection,
- tab_bar_x,
- tab_bar_y,
- tab_bar_width,
- ) {
- eprintln!("Failed to reposition tab bar: {:?}", e);
- }
- }
- }
- }
-
- for monitor_index in 0..self.tab_bars.len() {
- let has_visible_windows = self.windows.iter().any(|&window| {
- if let Some(client) = self.clients.get(&window) {
- if client.monitor_index != monitor_index
- || self.floating_windows.contains(&window)
- || self.fullscreen_windows.contains(&window)
- {
- return false;
- }
- if let Some(monitor) = self.monitors.get(monitor_index) {
- return (client.tags & monitor.tagset[monitor.selected_tags_index]) != 0;
- }
- }
- false
- });
-
- if is_tabbed && has_visible_windows {
- if let Err(e) = self.tab_bars[monitor_index].show(&self.connection) {
- eprintln!("Failed to show tab bar: {:?}", e);
- }
- } else if let Err(e) = self.tab_bars[monitor_index].hide(&self.connection) {
- eprintln!("Failed to hide tab bar: {:?}", e);
- }
- }
-
- if is_tabbed {
- self.update_tab_bars()?;
- }
-
- Ok(())
- }
-
- pub fn change_layout<L: Layout + 'static>(&mut self, new_layout: L) -> WmResult<()> {
- self.layout = Box::new(new_layout);
- self.apply_layout()?;
- Ok(())
- }
-
- fn send_configure_notify(&self, window: Window) -> WmResult<()> {
- let client = self.clients.get(&window);
- let (x, y, w, h, bw) = if let Some(c) = client {
- (
- c.x_position,
- c.y_position,
- c.width,
- c.height,
- c.border_width,
- )
- } else {
- let geom = self.connection.get_geometry(window)?.reply()?;
- (geom.x, geom.y, geom.width, geom.height, geom.border_width)
- };
-
- let event = ConfigureNotifyEvent {
- response_type: CONFIGURE_NOTIFY_EVENT,
- sequence: 0,
- event: window,
- window,
- above_sibling: x11rb::NONE,
- x,
- y,
- width: w,
- height: h,
- border_width: bw,
- override_redirect: false,
- };
-
- self.connection
- .send_event(false, window, EventMask::STRUCTURE_NOTIFY, event)?;
-
- Ok(())
- }
-
- fn update_size_hints(&mut self, window: Window) -> WmResult<()> {
- let size_hints = self
- .connection
- .get_property(
- false,
- window,
- x11rb::protocol::xproto::AtomEnum::WM_NORMAL_HINTS,
- x11rb::protocol::xproto::AtomEnum::WM_SIZE_HINTS,
- 0,
- 18,
- )?
- .reply()?;
-
- if size_hints.value.is_empty() {
- if let Some(client) = self.clients.get_mut(&window) {
- client.hints_valid = false;
- }
- return Ok(());
- }
-
- if size_hints.value.len() < 18 * 4 {
- if let Some(client) = self.clients.get_mut(&window) {
- client.hints_valid = false;
- }
- return Ok(());
- }
-
- use crate::size_hints::{flags::*, offset::*};
-
- let read_u32 = |offset: usize| -> u32 {
- let bytes = &size_hints.value[offset * 4..(offset + 1) * 4];
- u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
- };
-
- let flags = read_u32(FLAGS);
-
- if let Some(client) = self.clients.get_mut(&window) {
- if flags & P_BASE_SIZE != 0 {
- client.base_width = read_u32(BASE_WIDTH) as i32;
- client.base_height = read_u32(BASE_HEIGHT) as i32;
- } else if flags & P_MIN_SIZE != 0 {
- client.base_width = read_u32(MIN_WIDTH) as i32;
- client.base_height = read_u32(MIN_HEIGHT) as i32;
- } else {
- client.base_width = 0;
- client.base_height = 0;
- }
-
- if flags & P_RESIZE_INC != 0 {
- client.increment_width = read_u32(WIDTH_INC) as i32;
- client.increment_height = read_u32(HEIGHT_INC) as i32;
- } else {
- client.increment_width = 0;
- client.increment_height = 0;
- }
-
- if flags & P_MAX_SIZE != 0 {
- client.max_width = read_u32(MAX_WIDTH) as i32;
- client.max_height = read_u32(MAX_HEIGHT) as i32;
- } else {
- client.max_width = 0;
- client.max_height = 0;
- }
-
- if flags & P_MIN_SIZE != 0 {
- client.min_width = read_u32(MIN_WIDTH) as i32;
- client.min_height = read_u32(MIN_HEIGHT) as i32;
- } else if flags & P_BASE_SIZE != 0 {
- client.min_width = read_u32(BASE_WIDTH) as i32;
- client.min_height = read_u32(BASE_HEIGHT) as i32;
- } else {
- client.min_width = 0;
- client.min_height = 0;
- }
-
- if flags & P_ASPECT != 0 {
- client.min_aspect =
- (read_u32(MIN_ASPECT_Y) as f32) / (read_u32(MIN_ASPECT_X) as f32).max(1.0);
- client.max_aspect =
- (read_u32(MAX_ASPECT_X) as f32) / (read_u32(MAX_ASPECT_Y) as f32).max(1.0);
- } else {
- client.min_aspect = 0.0;
- client.max_aspect = 0.0;
- }
-
- client.is_fixed = client.max_width > 0
- && client.max_height > 0
- && client.max_width == client.min_width
- && client.max_height == client.min_height;
-
- client.hints_valid = true;
- }
- Ok(())
- }
-
- fn update_window_title(&mut self, window: Window) -> WmResult<()> {
- let net_name = self
- .connection
- .get_property(
- false,
- window,
- self.atoms.net_wm_name,
- self.atoms.utf8_string,
- 0,
- 256,
- )
- .ok()
- .and_then(|cookie| cookie.reply().ok());
-
- if let Some(name) = net_name
- && !name.value.is_empty()
- && let Ok(title) = String::from_utf8(name.value.clone())
- && let Some(client) = self.clients.get_mut(&window)
- {
- client.name = title;
- return Ok(());
- }
-
- let wm_name = self
- .connection
- .get_property(
- false,
- window,
- self.atoms.wm_name,
- x11rb::protocol::xproto::AtomEnum::STRING,
- 0,
- 256,
- )?
- .reply()?;
-
- if !wm_name.value.is_empty()
- && let Ok(title) = String::from_utf8(wm_name.value.clone())
- && let Some(client) = self.clients.get_mut(&window)
- {
- client.name = title;
- }
-
- Ok(())
- }
-
- fn update_window_hints(&mut self, window: Window) -> WmResult<()> {
- let hints_reply = self
- .connection
- .get_property(false, window, AtomEnum::WM_HINTS, AtomEnum::WM_HINTS, 0, 9)?
- .reply();
-
- if let Ok(hints) = hints_reply
- && hints.value.len() >= 4
- {
- let flags = u32::from_ne_bytes([
- hints.value[0],
- hints.value[1],
- hints.value[2],
- hints.value[3],
- ]);
-
- let selected_window = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- if Some(window) == selected_window && (flags & 256) != 0 {
- let new_flags = flags & !256;
- let mut new_hints = hints.value.clone();
- new_hints[0..4].copy_from_slice(&new_flags.to_ne_bytes());
-
- self.connection.change_property(
- x11rb::protocol::xproto::PropMode::REPLACE,
- window,
- AtomEnum::WM_HINTS,
- AtomEnum::WM_HINTS,
- 32,
- 9,
- &new_hints,
- )?;
- } else if let Some(client) = self.clients.get_mut(&window) {
- client.is_urgent = (flags & 256) != 0;
- }
-
- if hints.value.len() >= 8 && (flags & 1) != 0 {
- let input = i32::from_ne_bytes([
- hints.value[4],
- hints.value[5],
- hints.value[6],
- hints.value[7],
- ]);
-
- if let Some(client) = self.clients.get_mut(&window) {
- client.never_focus = input == 0;
- }
- } else if let Some(client) = self.clients.get_mut(&window) {
- client.never_focus = false;
- }
- }
-
- Ok(())
- }
-
- fn update_window_type(&mut self, window: Window) -> WmResult<()> {
- if let Ok(state_atoms) = self.get_window_atom_list_property(window, self.atoms.net_wm_state)
- {
- if state_atoms.contains(&self.atoms.net_wm_state_fullscreen) {
- self.set_window_fullscreen(window, true)?;
- }
- }
-
- if let Ok(Some(type_atom)) =
- self.get_window_atom_property(window, self.atoms.net_wm_window_type)
- && type_atom == self.atoms.net_wm_window_type_dialog
- {
- if let Some(client) = self.clients.get_mut(&window) {
- client.is_floating = true;
- }
- self.floating_windows.insert(window);
- }
-
- Ok(())
- }
-
- fn apply_size_hints(
- &mut self,
- window: Window,
- mut x: i32,
- mut y: i32,
- mut w: i32,
- mut h: i32,
- ) -> (i32, i32, i32, i32, bool) {
- let bh = 20;
-
- let (
- client_x,
- client_y,
- client_w,
- client_h,
- bw,
- monitor_index,
- is_floating,
- mut hints_valid,
- ) = {
- let client = match self.clients.get(&window) {
- Some(c) => c,
- None => return (x, y, w, h, false),
- };
- (
- client.x_position as i32,
- client.y_position as i32,
- client.width as i32,
- client.height as i32,
- client.border_width as i32,
- client.monitor_index,
- client.is_floating,
- client.hints_valid,
- )
- };
-
- let monitor = &self.monitors[monitor_index];
- let client_width = client_w + 2 * bw;
- let client_height = client_h + 2 * bw;
-
- w = w.max(1);
- h = h.max(1);
-
- if x >= monitor.window_area_x + monitor.window_area_width {
- x = monitor.window_area_x + monitor.window_area_width - client_width;
- }
- if y >= monitor.window_area_y + monitor.window_area_height {
- y = monitor.window_area_y + monitor.window_area_height - client_height;
- }
- if x + w + 2 * bw <= monitor.window_area_x {
- x = monitor.window_area_x;
- }
- if y + h + 2 * bw <= monitor.window_area_y {
- y = monitor.window_area_y;
- }
-
- if h < bh {
- h = bh;
- }
- if w < bh {
- w = bh;
- }
-
- if is_floating || self.layout.name() == "normie" {
- if !hints_valid {
- let _ = self.update_size_hints(window);
- hints_valid = self
- .clients
- .get(&window)
- .map(|c| c.hints_valid)
- .unwrap_or(false);
- }
-
- if hints_valid {
- let (
- base_width,
- base_height,
- min_width,
- min_height,
- max_width,
- max_height,
- inc_width,
- inc_height,
- min_aspect,
- max_aspect,
- ) = {
- let client = self.clients.get(&window).unwrap();
- (
- client.base_width,
- client.base_height,
- client.min_width,
- client.min_height,
- client.max_width,
- client.max_height,
- client.increment_width,
- client.increment_height,
- client.min_aspect,
- client.max_aspect,
- )
- };
-
- let base_is_min = base_width == min_width && base_height == min_height;
-
- if !base_is_min {
- w -= base_width;
- h -= base_height;
- }
-
- if min_aspect > 0.0 && max_aspect > 0.0 {
- if max_aspect < (w as f32 / h as f32) {
- w = (h as f32 * max_aspect + 0.5) as i32;
- } else if min_aspect < (h as f32 / w as f32) {
- h = (w as f32 * min_aspect + 0.5) as i32;
- }
- }
-
- if base_is_min {
- w -= base_width;
- h -= base_height;
- }
-
- if inc_width > 0 {
- w -= w % inc_width;
- }
- if inc_height > 0 {
- h -= h % inc_height;
- }
-
- w = (w + base_width).max(min_width);
- h = (h + base_height).max(min_height);
-
- if max_width > 0 {
- w = w.min(max_width);
- }
- if max_height > 0 {
- h = h.min(max_height);
- }
- }
- }
-
- let changed = x != client_x || y != client_y || w != client_w || h != client_h;
- (x, y, w, h, changed)
- }
-
- fn next_tiled(&self, start: Option<Window>, monitor: &Monitor) -> Option<Window> {
- let mut current = start;
- while let Some(window) = current {
- if let Some(client) = self.clients.get(&window) {
- let visible_tags = client.tags & monitor.tagset[monitor.selected_tags_index];
- if visible_tags != 0 && !client.is_floating {
- return Some(window);
- }
- current = client.next;
- } else {
- break;
- }
- }
- None
- }
-
- fn next_tagged(&self, start: Option<Window>, tags: u32) -> Option<Window> {
- let mut current = start;
- while let Some(window) = current {
- if let Some(client) = self.clients.get(&window) {
- let visible_on_tags = (client.tags & tags) != 0;
- if !client.is_floating && visible_on_tags {
- return Some(window);
- }
- current = client.next;
- } else {
- break;
- }
- }
- None
- }
-
- fn attach(&mut self, window: Window, monitor_index: usize) {
- if let Some(monitor) = self.monitors.get_mut(monitor_index)
- && let Some(client) = self.clients.get_mut(&window)
- {
- client.next = monitor.clients_head;
- monitor.clients_head = Some(window);
- }
- }
-
- fn attach_after(&mut self, window: Window, after_window: Window, monitor_index: usize) {
- if let Some(after_client) = self.clients.get(&after_window) {
- let old_next = after_client.next;
- if let Some(new_client) = self.clients.get_mut(&window) {
- new_client.next = old_next;
- }
- if let Some(after_client_mut) = self.clients.get_mut(&after_window) {
- after_client_mut.next = Some(window);
- }
- } else {
- self.attach(window, monitor_index);
- }
- }
-
- fn attach_aside(&mut self, window: Window, monitor_index: usize) {
- let monitor = match self.monitors.get(monitor_index) {
- Some(m) => m,
- None => return,
- };
-
- let new_window_tags = self.clients.get(&window).map(|c| c.tags).unwrap_or(0);
- let first_tagged = self.next_tagged(monitor.clients_head, new_window_tags);
-
- if first_tagged.is_none() {
- self.attach(window, monitor_index);
- return;
- }
-
- if let Some(insert_after_window) = first_tagged
- && let Some(after_client) = self.clients.get(&insert_after_window)
- {
- let old_next = after_client.next;
- if let Some(new_client) = self.clients.get_mut(&window) {
- new_client.next = old_next;
- }
- if let Some(after_client_mut) = self.clients.get_mut(&insert_after_window) {
- after_client_mut.next = Some(window);
- }
- }
- }
-
- fn detach(&mut self, window: Window) {
- let monitor_index = self.clients.get(&window).map(|c| c.monitor_index);
- if let Some(monitor_index) = monitor_index
- && let Some(monitor) = self.monitors.get_mut(monitor_index)
- {
- if monitor.clients_head == Some(window) {
- if let Some(client) = self.clients.get(&window) {
- monitor.clients_head = client.next;
- }
- } else {
- let mut current = monitor.clients_head;
- while let Some(current_window) = current {
- if let Some(current_client) = self.clients.get(¤t_window) {
- if current_client.next == Some(window) {
- let new_next = self.clients.get(&window).and_then(|c| c.next);
- if let Some(current_client_mut) = self.clients.get_mut(¤t_window)
- {
- current_client_mut.next = new_next;
- }
- break;
- }
- current = current_client.next;
- } else {
- break;
- }
- }
- }
- }
- }
-
- fn attach_stack(&mut self, window: Window, monitor_index: usize) {
- if let Some(monitor) = self.monitors.get_mut(monitor_index)
- && let Some(client) = self.clients.get_mut(&window)
- {
- client.stack_next = monitor.stack_head;
- monitor.stack_head = Some(window);
- }
- }
-
- fn detach_stack(&mut self, window: Window) {
- let monitor_index = self.clients.get(&window).map(|c| c.monitor_index);
- if let Some(monitor_index) = monitor_index
- && let Some(monitor) = self.monitors.get_mut(monitor_index)
- {
- if monitor.stack_head == Some(window) {
- if let Some(client) = self.clients.get(&window) {
- monitor.stack_head = client.stack_next;
- }
- let should_update_selected = monitor.selected_client == Some(window);
- let mut new_selected: Option<Window> = None;
- if should_update_selected {
- let mut stack_current = monitor.stack_head;
- while let Some(stack_window) = stack_current {
- if let Some(stack_client) = self.clients.get(&stack_window) {
- if self.is_window_visible(stack_window) {
- new_selected = Some(stack_window);
- break;
- }
- stack_current = stack_client.stack_next;
- } else {
- break;
- }
- }
- }
- if should_update_selected
- && let Some(monitor) = self.monitors.get_mut(monitor_index)
- {
- monitor.selected_client = new_selected;
- }
- } else {
- let mut current = monitor.stack_head;
- while let Some(current_window) = current {
- if let Some(current_client) = self.clients.get(¤t_window) {
- if current_client.stack_next == Some(window) {
- let new_stack_next =
- self.clients.get(&window).and_then(|c| c.stack_next);
- if let Some(current_client_mut) = self.clients.get_mut(¤t_window)
- {
- current_client_mut.stack_next = new_stack_next;
- }
- break;
- }
- current = current_client.stack_next;
- } else {
- break;
- }
- }
- }
- }
- }
-
- fn remove_window(&mut self, window: Window, destroyed: bool) -> WmResult<()> {
- let initial_count = self.windows.len();
-
- let focused = self
- .monitors
- .get(self.selected_monitor)
- .and_then(|m| m.selected_client);
-
- if !destroyed {
- if let Some(client) = self.clients.get(&window) {
- let old_border_width = client.old_border_width;
- self.connection.configure_window(
- window,
- &ConfigureWindowAux::new().border_width(old_border_width as u32),
- )?;
- }
- self.set_wm_state(window, 0)?;
- }
-
- if self.clients.contains_key(&window) {
- self.detach(window);
- self.detach_stack(window);
- self.clients.remove(&window);
- }
-
- self.windows.retain(|&w| w != window);
- self.floating_windows.remove(&window);
- self.update_client_list()?;
-
- if self.windows.len() < initial_count {
- if focused == Some(window) {
- let visible = self.visible_windows_on_monitor(self.selected_monitor);
- if let Some(&new_win) = visible.last() {
- self.focus(Some(new_win))?;
- if self.layout.name() == "scrolling" {
- self.scroll_to_window(new_win, true)?;
- }
- } else if let Some(monitor) = self.monitors.get_mut(self.selected_monitor) {
- monitor.selected_client = None;
- }
- }
-
- self.apply_layout()?;
- self.update_bar()?;
- }
- Ok(())
- }
-
- fn get_selected_monitor(&self) -> &Monitor {
- &self.monitors[self.selected_monitor]
- }
-
- fn has_windows_on_tag(&self, monitor_number: usize, tag_index: usize) -> bool {
- let Some(monitor) = self.monitors.get(monitor_number) else {
- return false;
- };
-
- let mut current = monitor.clients_head;
- while let Some(window) = current {
- let Some(client) = self.clients.get(&window) else {
- break;
- };
-
- if unmask_tag(client.tags) == tag_index {
- return true;
- }
- current = client.next;
- }
-
- false
- }
-
- fn run_autostart_commands(&self) {
- for command in &self.config.autostart {
- crate::signal::spawn_detached(command);
- eprintln!("[autostart] Spawned: {}", command);
- }
- }
-}
diff --git a/src/x11/display.zig b/src/x11/display.zig
new file mode 100644
index 0000000..10bebb8
--- /dev/null
+++ b/src/x11/display.zig
@@ -0,0 +1,100 @@
+const std = @import("std");
+const xlib = @import("xlib.zig");
+
+pub const DisplayError = error{
+ cannot_open_display,
+ another_wm_running,
+};
+
+var wm_detected: bool = false;
+
+pub const Display = struct {
+ handle: *xlib.Display,
+ screen: c_int,
+ root: xlib.Window,
+
+ pub fn open() DisplayError!Display {
+ const handle = xlib.XOpenDisplay(null) orelse return DisplayError.cannot_open_display;
+ const screen = xlib.XDefaultScreen(handle);
+ const root = xlib.XRootWindow(handle, screen);
+
+ return Display{
+ .handle = handle,
+ .screen = screen,
+ .root = root,
+ };
+ }
+
+ pub fn close(self: *Display) void {
+ _ = xlib.XCloseDisplay(self.handle);
+ }
+
+ pub fn become_window_manager(self: *Display) DisplayError!void {
+ wm_detected = false;
+ _ = xlib.XSetErrorHandler(on_wm_detected);
+ _ = xlib.XSelectInput(
+ self.handle,
+ self.root,
+ xlib.SubstructureRedirectMask | xlib.SubstructureNotifyMask | xlib.ButtonPressMask | xlib.PointerMotionMask | xlib.EnterWindowMask,
+ );
+ _ = xlib.XSync(self.handle, xlib.False);
+
+ if (wm_detected) {
+ return DisplayError.another_wm_running;
+ }
+
+ _ = xlib.XSetErrorHandler(on_x_error);
+ }
+
+ pub fn screen_width(self: *Display) c_int {
+ return xlib.XDisplayWidth(self.handle, self.screen);
+ }
+
+ pub fn screen_height(self: *Display) c_int {
+ return xlib.XDisplayHeight(self.handle, self.screen);
+ }
+
+ pub fn next_event(self: *Display) xlib.XEvent {
+ var event: xlib.XEvent = undefined;
+ _ = xlib.XNextEvent(self.handle, &event);
+ return event;
+ }
+
+ pub fn pending(self: *Display) c_int {
+ return xlib.XPending(self.handle);
+ }
+
+ pub fn sync(self: *Display, discard: bool) void {
+ _ = xlib.XSync(self.handle, if (discard) xlib.True else xlib.False);
+ }
+
+ pub fn grab_key(
+ self: *Display,
+ keycode: c_int,
+ modifiers: c_uint,
+ ) void {
+ _ = xlib.XGrabKey(
+ self.handle,
+ keycode,
+ modifiers,
+ self.root,
+ xlib.True,
+ xlib.GrabModeAsync,
+ xlib.GrabModeAsync,
+ );
+ }
+
+ pub fn keysym_to_keycode(self: *Display, keysym: xlib.KeySym) c_int {
+ return @intCast(xlib.XKeysymToKeycode(self.handle, keysym));
+ }
+};
+
+fn on_wm_detected(_: ?*xlib.Display, _: [*c]xlib.XErrorEvent) callconv(.c) c_int {
+ wm_detected = true;
+ return 0;
+}
+
+fn on_x_error(_: ?*xlib.Display, event: [*c]xlib.XErrorEvent) callconv(.c) c_int {
+ std.debug.print("x11 error: request={d} error={d}\n", .{ event.*.request_code, event.*.error_code });
+ return 0;
+}
diff --git a/src/x11/events.zig b/src/x11/events.zig
new file mode 100644
index 0000000..15a3e86
--- /dev/null
+++ b/src/x11/events.zig
@@ -0,0 +1,84 @@
+const std = @import("std");
+const xlib = @import("xlib.zig");
+
+pub const EventType = enum(c_int) {
+ key_press = xlib.KeyPress,
+ key_release = xlib.KeyRelease,
+ button_press = xlib.ButtonPress,
+ button_release = xlib.ButtonRelease,
+ motion_notify = xlib.MotionNotify,
+ enter_notify = xlib.EnterNotify,
+ leave_notify = xlib.LeaveNotify,
+ focus_in = xlib.FocusIn,
+ focus_out = xlib.FocusOut,
+ keymap_notify = xlib.KeymapNotify,
+ expose = xlib.Expose,
+ graphics_expose = xlib.GraphicsExpose,
+ no_expose = xlib.NoExpose,
+ visibility_notify = xlib.VisibilityNotify,
+ create_notify = xlib.CreateNotify,
+ destroy_notify = xlib.DestroyNotify,
+ unmap_notify = xlib.UnmapNotify,
+ map_notify = xlib.MapNotify,
+ map_request = xlib.MapRequest,
+ reparent_notify = xlib.ReparentNotify,
+ configure_notify = xlib.ConfigureNotify,
+ configure_request = xlib.ConfigureRequest,
+ gravity_notify = xlib.GravityNotify,
+ resize_request = xlib.ResizeRequest,
+ circulate_notify = xlib.CirculateNotify,
+ circulate_request = xlib.CirculateRequest,
+ property_notify = xlib.PropertyNotify,
+ selection_clear = xlib.SelectionClear,
+ selection_request = xlib.SelectionRequest,
+ selection_notify = xlib.SelectionNotify,
+ colormap_notify = xlib.ColormapNotify,
+ client_message = xlib.ClientMessage,
+ mapping_notify = xlib.MappingNotify,
+ generic_event = xlib.GenericEvent,
+ _,
+};
+
+pub fn get_event_type(event: *const xlib.XEvent) EventType {
+ return @enumFromInt(event.type);
+}
+
+pub fn event_name(event_type: EventType) []const u8 {
+ return switch (event_type) {
+ .key_press => "key_press",
+ .key_release => "key_release",
+ .button_press => "button_press",
+ .button_release => "button_release",
+ .motion_notify => "motion_notify",
+ .enter_notify => "enter_notify",
+ .leave_notify => "leave_notify",
+ .focus_in => "focus_in",
+ .focus_out => "focus_out",
+ .keymap_notify => "keymap_notify",
+ .expose => "expose",
+ .graphics_expose => "graphics_expose",
+ .no_expose => "no_expose",
+ .visibility_notify => "visibility_notify",
+ .create_notify => "create_notify",
+ .destroy_notify => "destroy_notify",
+ .unmap_notify => "unmap_notify",
+ .map_notify => "map_notify",
+ .map_request => "map_request",
+ .reparent_notify => "reparent_notify",
+ .configure_notify => "configure_notify",
+ .configure_request => "configure_request",
+ .gravity_notify => "gravity_notify",
+ .resize_request => "resize_request",
+ .circulate_notify => "circulate_notify",
+ .circulate_request => "circulate_request",
+ .property_notify => "property_notify",
+ .selection_clear => "selection_clear",
+ .selection_request => "selection_request",
+ .selection_notify => "selection_notify",
+ .colormap_notify => "colormap_notify",
+ .client_message => "client_message",
+ .mapping_notify => "mapping_notify",
+ .generic_event => "generic_event",
+ _ => "unknown",
+ };
+}
diff --git a/src/x11/xlib.zig b/src/x11/xlib.zig
new file mode 100644
index 0000000..50065d1
--- /dev/null
+++ b/src/x11/xlib.zig
@@ -0,0 +1,268 @@
+pub const c = @cImport({
+ @cInclude("X11/Xlib.h");
+ @cInclude("X11/Xutil.h");
+ @cInclude("X11/Xatom.h");
+ @cInclude("X11/cursorfont.h");
+ @cInclude("X11/keysym.h");
+ @cInclude("X11/extensions/Xinerama.h");
+ @cInclude("X11/Xft/Xft.h");
+});
+
+pub const Display = c.Display;
+pub const Window = c.Window;
+pub const XEvent = c.XEvent;
+pub const XWindowAttributes = c.XWindowAttributes;
+pub const XWindowChanges = c.XWindowChanges;
+pub const XMapRequestEvent = c.XMapRequestEvent;
+pub const XConfigureRequestEvent = c.XConfigureRequestEvent;
+pub const XKeyEvent = c.XKeyEvent;
+pub const XDestroyWindowEvent = c.XDestroyWindowEvent;
+pub const XUnmapEvent = c.XUnmapEvent;
+pub const XCrossingEvent = c.XCrossingEvent;
+pub const XFocusChangeEvent = c.XFocusChangeEvent;
+pub const XErrorEvent = c.XErrorEvent;
+pub const KeySym = c.KeySym;
+
+pub const XOpenDisplay = c.XOpenDisplay;
+pub const XCloseDisplay = c.XCloseDisplay;
+pub const XConnectionNumber = c.XConnectionNumber;
+pub const XDefaultScreen = c.XDefaultScreen;
+pub const XRootWindow = c.XRootWindow;
+pub const XDisplayWidth = c.XDisplayWidth;
+pub const XDisplayHeight = c.XDisplayHeight;
+pub const XNextEvent = c.XNextEvent;
+pub const XPending = c.XPending;
+pub const XSync = c.XSync;
+pub const XSelectInput = c.XSelectInput;
+pub const XSetErrorHandler = c.XSetErrorHandler;
+pub const XGrabKey = c.XGrabKey;
+pub const XKeysymToKeycode = c.XKeysymToKeycode;
+pub const XKeycodeToKeysym = c.XKeycodeToKeysym;
+pub const XQueryTree = c.XQueryTree;
+pub const XFree = c.XFree;
+pub const XGetWindowAttributes = c.XGetWindowAttributes;
+pub const XMapWindow = c.XMapWindow;
+pub const XConfigureWindow = c.XConfigureWindow;
+pub const XSetInputFocus = c.XSetInputFocus;
+pub const XRaiseWindow = c.XRaiseWindow;
+pub const XMoveResizeWindow = c.XMoveResizeWindow;
+pub const XMoveWindow = c.XMoveWindow;
+pub const XSetWindowBorder = c.XSetWindowBorder;
+pub const XSetWindowBorderWidth = c.XSetWindowBorderWidth;
+
+pub const SubstructureRedirectMask = c.SubstructureRedirectMask;
+pub const SubstructureNotifyMask = c.SubstructureNotifyMask;
+pub const EnterWindowMask = c.EnterWindowMask;
+pub const FocusChangeMask = c.FocusChangeMask;
+pub const PropertyChangeMask = c.PropertyChangeMask;
+pub const StructureNotifyMask = c.StructureNotifyMask;
+
+pub const Mod4Mask = c.Mod4Mask;
+pub const ShiftMask = c.ShiftMask;
+pub const LockMask = c.LockMask;
+pub const Mod2Mask = c.Mod2Mask;
+pub const ControlMask = c.ControlMask;
+
+pub const GrabModeAsync = c.GrabModeAsync;
+pub const RevertToPointerRoot = c.RevertToPointerRoot;
+pub const CurrentTime = c.CurrentTime;
+pub const NotifyNormal = c.NotifyNormal;
+pub const NotifyInferior = c.NotifyInferior;
+
+pub const True = c.True;
+pub const False = c.False;
+
+pub const XK_q = c.XK_q;
+pub const XK_f = c.XK_f;
+pub const XK_h = c.XK_h;
+pub const XK_i = c.XK_i;
+pub const XK_d = c.XK_d;
+pub const XK_j = c.XK_j;
+pub const XK_k = c.XK_k;
+pub const XK_l = c.XK_l;
+pub const XK_m = c.XK_m;
+pub const XK_comma = c.XK_comma;
+pub const XK_period = c.XK_period;
+pub const XK_space = c.XK_space;
+pub const XK_Return = c.XK_Return;
+pub const XK_p = c.XK_p;
+pub const XK_a = c.XK_a;
+pub const XK_s = c.XK_s;
+pub const XK_1 = c.XK_1;
+pub const XK_2 = c.XK_2;
+pub const XK_3 = c.XK_3;
+pub const XK_4 = c.XK_4;
+pub const XK_5 = c.XK_5;
+pub const XK_6 = c.XK_6;
+pub const XK_7 = c.XK_7;
+pub const XK_8 = c.XK_8;
+pub const XK_9 = c.XK_9;
+
+pub const Mod1Mask = c.Mod1Mask;
+pub const Mod3Mask = c.Mod3Mask;
+pub const Mod5Mask = c.Mod5Mask;
+
+pub const XKillClient = c.XKillClient;
+pub const XInternAtom = c.XInternAtom;
+pub const XChangeProperty = c.XChangeProperty;
+pub const XGetWindowProperty = c.XGetWindowProperty;
+pub const XSendEvent = c.XSendEvent;
+
+pub const Atom = c.Atom;
+pub const XA_ATOM = c.XA_ATOM;
+pub const XClientMessageEvent = c.XClientMessageEvent;
+
+pub const PropModeReplace = c.PropModeReplace;
+
+pub const XGrabPointer = c.XGrabPointer;
+pub const XUngrabPointer = c.XUngrabPointer;
+pub const XGrabButton = c.XGrabButton;
+pub const XQueryPointer = c.XQueryPointer;
+pub const XWarpPointer = c.XWarpPointer;
+pub const XGetModifierMapping = c.XGetModifierMapping;
+pub const XFreeModifiermap = c.XFreeModifiermap;
+pub const XModifierKeymap = c.XModifierKeymap;
+pub const XK_Num_Lock = c.XK_Num_Lock;
+
+pub const Button1 = c.Button1;
+pub const Button1Mask = c.Button1Mask;
+pub const Button3 = c.Button3;
+pub const Button3Mask = c.Button3Mask;
+pub const ButtonPressMask = c.ButtonPressMask;
+pub const ButtonReleaseMask = c.ButtonReleaseMask;
+pub const PointerMotionMask = c.PointerMotionMask;
+pub const GrabModeSync = c.GrabModeSync;
+pub const GrabSuccess = c.GrabSuccess;
+pub const None = c.None;
+
+pub const XButtonEvent = c.XButtonEvent;
+pub const XMotionEvent = c.XMotionEvent;
+pub const XExposeEvent = c.XExposeEvent;
+
+pub const XineramaIsActive = c.XineramaIsActive;
+pub const XineramaQueryScreens = c.XineramaQueryScreens;
+pub const XineramaScreenInfo = c.XineramaScreenInfo;
+
+pub const XftFont = c.XftFont;
+pub const XftColor = c.XftColor;
+pub const XftDraw = c.XftDraw;
+pub const XftFontOpenName = c.XftFontOpenName;
+pub const XftFontClose = c.XftFontClose;
+pub const XftDrawCreate = c.XftDrawCreate;
+pub const XftDrawDestroy = c.XftDrawDestroy;
+pub const XftDrawStringUtf8 = c.XftDrawStringUtf8;
+pub const XftColorAllocValue = c.XftColorAllocValue;
+pub const XftColorFree = c.XftColorFree;
+pub const XftTextExtentsUtf8 = c.XftTextExtentsUtf8;
+pub const XGlyphInfo = c.XGlyphInfo;
+pub const XRenderColor = c.XRenderColor;
+
+pub const XCreatePixmap = c.XCreatePixmap;
+pub const XFreePixmap = c.XFreePixmap;
+pub const XCopyArea = c.XCopyArea;
+pub const XCreateGC = c.XCreateGC;
+pub const XFreeGC = c.XFreeGC;
+pub const XSetForeground = c.XSetForeground;
+pub const XFillRectangle = c.XFillRectangle;
+pub const XDefaultVisual = c.XDefaultVisual;
+pub const XDefaultColormap = c.XDefaultColormap;
+pub const XDefaultDepth = c.XDefaultDepth;
+pub const Pixmap = c.Pixmap;
+pub const Drawable = c.Drawable;
+pub const GC = c.GC;
+pub const Visual = c.Visual;
+pub const Colormap = c.Colormap;
+
+pub const KeyPress = c.KeyPress;
+pub const KeyRelease = c.KeyRelease;
+pub const ButtonPress = c.ButtonPress;
+pub const ButtonRelease = c.ButtonRelease;
+pub const MotionNotify = c.MotionNotify;
+pub const EnterNotify = c.EnterNotify;
+pub const LeaveNotify = c.LeaveNotify;
+pub const FocusIn = c.FocusIn;
+pub const FocusOut = c.FocusOut;
+pub const KeymapNotify = c.KeymapNotify;
+pub const Expose = c.Expose;
+pub const GraphicsExpose = c.GraphicsExpose;
+pub const NoExpose = c.NoExpose;
+pub const VisibilityNotify = c.VisibilityNotify;
+pub const CreateNotify = c.CreateNotify;
+pub const DestroyNotify = c.DestroyNotify;
+pub const UnmapNotify = c.UnmapNotify;
+pub const MapNotify = c.MapNotify;
+pub const MapRequest = c.MapRequest;
+pub const ReparentNotify = c.ReparentNotify;
+pub const ConfigureNotify = c.ConfigureNotify;
+pub const ConfigureRequest = c.ConfigureRequest;
+pub const GravityNotify = c.GravityNotify;
+pub const ResizeRequest = c.ResizeRequest;
+pub const CirculateNotify = c.CirculateNotify;
+pub const CirculateRequest = c.CirculateRequest;
+pub const PropertyNotify = c.PropertyNotify;
+pub const SelectionClear = c.SelectionClear;
+pub const SelectionRequest = c.SelectionRequest;
+pub const SelectionNotify = c.SelectionNotify;
+pub const ColormapNotify = c.ColormapNotify;
+pub const ClientMessage = c.ClientMessage;
+pub const MappingNotify = c.MappingNotify;
+pub const GenericEvent = c.GenericEvent;
+
+pub const XClassHint = c.XClassHint;
+pub const XGetClassHint = c.XGetClassHint;
+pub const XWMHints = c.XWMHints;
+pub const XGetWMHints = c.XGetWMHints;
+pub const XSetWMHints = c.XSetWMHints;
+pub const XSizeHints = c.XSizeHints;
+pub const XGetWMNormalHints = c.XGetWMNormalHints;
+pub const XGetTransientForHint = c.XGetTransientForHint;
+pub const XTextProperty = c.XTextProperty;
+pub const XGetTextProperty = c.XGetTextProperty;
+pub const XmbTextPropertyToTextList = c.XmbTextPropertyToTextList;
+pub const XFreeStringList = c.XFreeStringList;
+pub const Success = c.Success;
+pub const XGetWMProtocols = c.XGetWMProtocols;
+pub const XAllocSizeHints = c.XAllocSizeHints;
+
+pub const XUrgencyHint = c.XUrgencyHint;
+pub const InputHint = c.InputHint;
+pub const PBaseSize = c.PBaseSize;
+pub const PMinSize = c.PMinSize;
+pub const PMaxSize = c.PMaxSize;
+pub const PResizeInc = c.PResizeInc;
+pub const PAspect = c.PAspect;
+pub const PSize = c.PSize;
+
+pub const XA_WM_NAME = c.XA_WM_NAME;
+pub const XA_WINDOW = c.XA_WINDOW;
+pub const XA_STRING = c.XA_STRING;
+pub const PropModeAppend = c.PropModeAppend;
+pub const NoEventMask = c.NoEventMask;
+
+pub const XPropertyEvent = c.XPropertyEvent;
+pub const PropertyDelete = c.PropertyDelete;
+pub const XA_WM_TRANSIENT_FOR = c.XA_WM_TRANSIENT_FOR;
+pub const XA_WM_NORMAL_HINTS = c.XA_WM_NORMAL_HINTS;
+pub const XA_WM_HINTS = c.XA_WM_HINTS;
+
+pub const XDeleteProperty = c.XDeleteProperty;
+pub const XCreateSimpleWindow = c.XCreateSimpleWindow;
+pub const XDestroyWindow = c.XDestroyWindow;
+pub const XGrabServer = c.XGrabServer;
+pub const XUngrabServer = c.XUngrabServer;
+pub const XUngrabButton = c.XUngrabButton;
+pub const XUngrabKey = c.XUngrabKey;
+pub const AnyKey = c.AnyKey;
+pub const AnyModifier = c.AnyModifier;
+
+pub const Cursor = c.Cursor;
+pub const XCreateFontCursor = c.XCreateFontCursor;
+pub const XFreeCursor = c.XFreeCursor;
+pub const XDefineCursor = c.XDefineCursor;
+pub const XC_left_ptr = c.XC_left_ptr;
+pub const XC_sizing = c.XC_sizing;
+pub const XC_fleur = c.XC_fleur;
+
+pub const XAllowEvents = c.XAllowEvents;
+pub const ReplayPointer = c.ReplayPointer;
+pub const AnyButton = c.AnyButton;
diff --git a/tests/config_tests.zig b/tests/config_tests.zig
new file mode 100644
index 0000000..230e971
--- /dev/null
+++ b/tests/config_tests.zig
@@ -0,0 +1,88 @@
+const std = @import("std");
+const testing = std.testing;
+
+const Config = struct {
+ terminal: []const u8 = "st",
+ font: []const u8 = "monospace:size=10",
+ border_width: i32 = 2,
+ border_focused: u32 = 0x6dade3,
+ border_unfocused: u32 = 0x444444,
+ gaps_enabled: bool = true,
+ gap_inner_h: i32 = 5,
+ gap_inner_v: i32 = 5,
+ gap_outer_h: i32 = 5,
+ gap_outer_v: i32 = 5,
+ modkey: u32 = (1 << 6),
+ auto_tile: bool = false,
+ tag_back_and_forth: bool = false,
+ hide_vacant_tags: bool = false,
+};
+
+test "config has correct default terminal" {
+ const cfg = Config{};
+ try testing.expectEqualStrings("st", cfg.terminal);
+}
+
+test "config has correct default font" {
+ const cfg = Config{};
+ try testing.expectEqualStrings("monospace:size=10", cfg.font);
+}
+
+test "config has correct default border width" {
+ const cfg = Config{};
+ try testing.expectEqual(@as(i32, 2), cfg.border_width);
+}
+
+test "config has correct default focused border color" {
+ const cfg = Config{};
+ try testing.expectEqual(@as(u32, 0x6dade3), cfg.border_focused);
+}
+
+test "config has correct default unfocused border color" {
+ const cfg = Config{};
+ try testing.expectEqual(@as(u32, 0x444444), cfg.border_unfocused);
+}
+
+test "config gaps are enabled by default" {
+ const cfg = Config{};
+ try testing.expect(cfg.gaps_enabled);
+}
+
+test "config has correct default gap values" {
+ const cfg = Config{};
+ try testing.expectEqual(@as(i32, 5), cfg.gap_inner_h);
+ try testing.expectEqual(@as(i32, 5), cfg.gap_inner_v);
+ try testing.expectEqual(@as(i32, 5), cfg.gap_outer_h);
+ try testing.expectEqual(@as(i32, 5), cfg.gap_outer_v);
+}
+
+test "config auto_tile is disabled by default" {
+ const cfg = Config{};
+ try testing.expect(!cfg.auto_tile);
+}
+
+test "config tag_back_and_forth is disabled by default" {
+ const cfg = Config{};
+ try testing.expect(!cfg.tag_back_and_forth);
+}
+
+test "config hide_vacant_tags is disabled by default" {
+ const cfg = Config{};
+ try testing.expect(!cfg.hide_vacant_tags);
+}
+
+test "config default modkey is Mod4 (super)" {
+ const cfg = Config{};
+ try testing.expectEqual(@as(u32, 1 << 6), cfg.modkey);
+}
+
+test "can override config defaults" {
+ const cfg = Config{
+ .terminal = "alacritty",
+ .border_width = 3,
+ .gaps_enabled = false,
+ };
+ try testing.expectEqualStrings("alacritty", cfg.terminal);
+ try testing.expectEqual(@as(i32, 3), cfg.border_width);
+ try testing.expect(!cfg.gaps_enabled);
+}
diff --git a/tests/keybind_tests.zig b/tests/keybind_tests.zig
new file mode 100644
index 0000000..ad8814c
--- /dev/null
+++ b/tests/keybind_tests.zig
@@ -0,0 +1,137 @@
+const std = @import("std");
+const testing = std.testing;
+
+const Mod4Mask: u32 = 1 << 6;
+const Mod1Mask: u32 = 1 << 3;
+const ShiftMask: u32 = 1 << 0;
+const ControlMask: u32 = 1 << 2;
+
+fn parse_modifier(name: []const u8) u32 {
+ if (std.mem.eql(u8, name, "Mod4") or std.mem.eql(u8, name, "mod4") or std.mem.eql(u8, name, "super")) {
+ return Mod4Mask;
+ } else if (std.mem.eql(u8, name, "Mod1") or std.mem.eql(u8, name, "mod1") or std.mem.eql(u8, name, "alt")) {
+ return Mod1Mask;
+ } else if (std.mem.eql(u8, name, "Shift") or std.mem.eql(u8, name, "shift")) {
+ return ShiftMask;
+ } else if (std.mem.eql(u8, name, "Control") or std.mem.eql(u8, name, "control") or std.mem.eql(u8, name, "ctrl")) {
+ return ControlMask;
+ }
+ return 0;
+}
+
+fn parse_modifiers(names: []const []const u8) u32 {
+ var mask: u32 = 0;
+ for (names) |name| {
+ mask |= parse_modifier(name);
+ }
+ return mask;
+}
+
+test "parse_modifier: Mod4 variants" {
+ try testing.expectEqual(Mod4Mask, parse_modifier("Mod4"));
+ try testing.expectEqual(Mod4Mask, parse_modifier("mod4"));
+ try testing.expectEqual(Mod4Mask, parse_modifier("super"));
+}
+
+test "parse_modifier: Mod1 (Alt) variants" {
+ try testing.expectEqual(Mod1Mask, parse_modifier("Mod1"));
+ try testing.expectEqual(Mod1Mask, parse_modifier("mod1"));
+ try testing.expectEqual(Mod1Mask, parse_modifier("alt"));
+}
+
+test "parse_modifier: Shift variants" {
+ try testing.expectEqual(ShiftMask, parse_modifier("Shift"));
+ try testing.expectEqual(ShiftMask, parse_modifier("shift"));
+}
+
+test "parse_modifier: Control variants" {
+ try testing.expectEqual(ControlMask, parse_modifier("Control"));
+ try testing.expectEqual(ControlMask, parse_modifier("control"));
+ try testing.expectEqual(ControlMask, parse_modifier("ctrl"));
+}
+
+test "parse_modifier: unknown returns 0" {
+ try testing.expectEqual(@as(u32, 0), parse_modifier("Unknown"));
+ try testing.expectEqual(@as(u32, 0), parse_modifier(""));
+}
+
+test "parse_modifiers: combines multiple modifiers" {
+ const mods = &[_][]const u8{ "Mod4", "Shift" };
+ try testing.expectEqual(Mod4Mask | ShiftMask, parse_modifiers(mods));
+}
+
+test "parse_modifiers: triple combination" {
+ const mods = &[_][]const u8{ "super", "shift", "ctrl" };
+ try testing.expectEqual(Mod4Mask | ShiftMask | ControlMask, parse_modifiers(mods));
+}
+
+test "parse_modifiers: empty list returns 0" {
+ const mods = &[_][]const u8{};
+ try testing.expectEqual(@as(u32, 0), parse_modifiers(mods));
+}
+
+fn key_name_to_keysym(name: []const u8) ?u64 {
+ const key_map = .{
+ .{ "Return", 0xff0d },
+ .{ "Enter", 0xff0d },
+ .{ "Tab", 0xff09 },
+ .{ "Escape", 0xff1b },
+ .{ "BackSpace", 0xff08 },
+ .{ "Delete", 0xffff },
+ .{ "space", 0x0020 },
+ .{ "Space", 0x0020 },
+ };
+
+ inline for (key_map) |entry| {
+ if (std.mem.eql(u8, name, entry[0])) {
+ return entry[1];
+ }
+ }
+
+ if (name.len == 1) {
+ const char = name[0];
+ if (char >= 'a' and char <= 'z') {
+ return char;
+ }
+ if (char >= 'A' and char <= 'Z') {
+ return char + 32;
+ }
+ if (char >= '0' and char <= '9') {
+ return char;
+ }
+ }
+
+ return null;
+}
+
+test "key_name_to_keysym: special keys" {
+ try testing.expectEqual(@as(?u64, 0xff0d), key_name_to_keysym("Return"));
+ try testing.expectEqual(@as(?u64, 0xff0d), key_name_to_keysym("Enter"));
+ try testing.expectEqual(@as(?u64, 0xff09), key_name_to_keysym("Tab"));
+ try testing.expectEqual(@as(?u64, 0xff1b), key_name_to_keysym("Escape"));
+}
+
+test "key_name_to_keysym: lowercase letters" {
+ try testing.expectEqual(@as(?u64, 'a'), key_name_to_keysym("a"));
+ try testing.expectEqual(@as(?u64, 'z'), key_name_to_keysym("z"));
+}
+
+test "key_name_to_keysym: uppercase converts to lowercase" {
+ try testing.expectEqual(@as(?u64, 'a'), key_name_to_keysym("A"));
+ try testing.expectEqual(@as(?u64, 'z'), key_name_to_keysym("Z"));
+}
+
+test "key_name_to_keysym: numbers" {
+ try testing.expectEqual(@as(?u64, '0'), key_name_to_keysym("0"));
+ try testing.expectEqual(@as(?u64, '9'), key_name_to_keysym("9"));
+}
+
+test "key_name_to_keysym: unknown returns null" {
+ try testing.expectEqual(@as(?u64, null), key_name_to_keysym("UnknownKey"));
+ try testing.expectEqual(@as(?u64, null), key_name_to_keysym(""));
+}
+
+test "key_name_to_keysym: space variants" {
+ try testing.expectEqual(@as(?u64, 0x0020), key_name_to_keysym("space"));
+ try testing.expectEqual(@as(?u64, 0x0020), key_name_to_keysym("Space"));
+}
diff --git a/tests/layout_tests.zig b/tests/layout_tests.zig
new file mode 100644
index 0000000..3481b62
--- /dev/null
+++ b/tests/layout_tests.zig
@@ -0,0 +1,138 @@
+const std = @import("std");
+const testing = std.testing;
+
+const Rect = struct {
+ x: i32,
+ y: i32,
+ w: i32,
+ h: i32,
+};
+
+fn calculate_master_stack_layout(
+ area: Rect,
+ num_clients: usize,
+ num_master: usize,
+ master_factor: f32,
+ gap: i32,
+) []Rect {
+ var rects: [16]Rect = undefined;
+ if (num_clients == 0) return rects[0..0];
+
+ const actual_master = @min(num_master, num_clients);
+ const stack_count = if (num_clients > actual_master) num_clients - actual_master else 0;
+
+ const master_width: i32 = if (stack_count > 0)
+ @intFromFloat(@as(f32, @floatFromInt(area.w - gap)) * master_factor)
+ else
+ area.w;
+
+ var i: usize = 0;
+ while (i < actual_master) : (i += 1) {
+ const h = @divTrunc(area.h - @as(i32, @intCast(actual_master - 1)) * gap, @as(i32, @intCast(actual_master)));
+ rects[i] = .{
+ .x = area.x,
+ .y = area.y + @as(i32, @intCast(i)) * (h + gap),
+ .w = master_width,
+ .h = h,
+ };
+ }
+
+ const stack_width = area.w - master_width - gap;
+ var j: usize = 0;
+ while (j < stack_count) : (j += 1) {
+ const h = @divTrunc(area.h - @as(i32, @intCast(stack_count - 1)) * gap, @as(i32, @intCast(stack_count)));
+ rects[actual_master + j] = .{
+ .x = area.x + master_width + gap,
+ .y = area.y + @as(i32, @intCast(j)) * (h + gap),
+ .w = stack_width,
+ .h = h,
+ };
+ }
+
+ return rects[0..num_clients];
+}
+
+test "tiling layout: single window fills area" {
+ const area = Rect{ .x = 0, .y = 0, .w = 800, .h = 600 };
+ const rects = calculate_master_stack_layout(area, 1, 1, 0.55, 5);
+
+ try testing.expectEqual(@as(usize, 1), rects.len);
+ try testing.expectEqual(@as(i32, 0), rects[0].x);
+ try testing.expectEqual(@as(i32, 0), rects[0].y);
+ try testing.expectEqual(@as(i32, 800), rects[0].w);
+ try testing.expectEqual(@as(i32, 600), rects[0].h);
+}
+
+test "tiling layout: two windows split horizontally" {
+ const area = Rect{ .x = 0, .y = 0, .w = 800, .h = 600 };
+ const rects = calculate_master_stack_layout(area, 2, 1, 0.5, 0);
+
+ try testing.expectEqual(@as(usize, 2), rects.len);
+ try testing.expectEqual(@as(i32, 400), rects[0].w);
+ try testing.expectEqual(@as(i32, 400), rects[1].w);
+}
+
+test "tiling layout: respects master factor" {
+ const area = Rect{ .x = 0, .y = 0, .w = 1000, .h = 600 };
+ const rects = calculate_master_stack_layout(area, 2, 1, 0.6, 0);
+
+ try testing.expectEqual(@as(usize, 2), rects.len);
+ try testing.expectEqual(@as(i32, 600), rects[0].w);
+ try testing.expectEqual(@as(i32, 400), rects[1].w);
+}
+
+test "tiling layout: no clients returns empty" {
+ const area = Rect{ .x = 0, .y = 0, .w = 800, .h = 600 };
+ const rects = calculate_master_stack_layout(area, 0, 1, 0.55, 5);
+
+ try testing.expectEqual(@as(usize, 0), rects.len);
+}
+
+test "tiling layout: multiple stack windows divide height" {
+ const area = Rect{ .x = 0, .y = 0, .w = 800, .h = 600 };
+ const rects = calculate_master_stack_layout(area, 3, 1, 0.5, 0);
+
+ try testing.expectEqual(@as(usize, 3), rects.len);
+ try testing.expectEqual(@as(i32, 600), rects[0].h);
+ try testing.expectEqual(@as(i32, 300), rects[1].h);
+ try testing.expectEqual(@as(i32, 300), rects[2].h);
+}
+
+fn calculate_monocle_layout(area: Rect, num_clients: usize) []Rect {
+ var rects: [16]Rect = undefined;
+ if (num_clients == 0) return rects[0..0];
+
+ var i: usize = 0;
+ while (i < num_clients) : (i += 1) {
+ rects[i] = area;
+ }
+ return rects[0..num_clients];
+}
+
+test "monocle layout: all windows get full area" {
+ const area = Rect{ .x = 0, .y = 0, .w = 800, .h = 600 };
+ const rects = calculate_monocle_layout(area, 3);
+
+ try testing.expectEqual(@as(usize, 3), rects.len);
+ for (rects) |rect| {
+ try testing.expectEqual(@as(i32, 0), rect.x);
+ try testing.expectEqual(@as(i32, 0), rect.y);
+ try testing.expectEqual(@as(i32, 800), rect.w);
+ try testing.expectEqual(@as(i32, 600), rect.h);
+ }
+}
+
+test "monocle layout: no clients returns empty" {
+ const area = Rect{ .x = 0, .y = 0, .w = 800, .h = 600 };
+ const rects = calculate_monocle_layout(area, 0);
+
+ try testing.expectEqual(@as(usize, 0), rects.len);
+}
+
+test "monocle layout: respects area offset" {
+ const area = Rect{ .x = 100, .y = 50, .w = 800, .h = 600 };
+ const rects = calculate_monocle_layout(area, 1);
+
+ try testing.expectEqual(@as(i32, 100), rects[0].x);
+ try testing.expectEqual(@as(i32, 50), rects[0].y);
+}
diff --git a/tests/main_tests.zig b/tests/main_tests.zig
new file mode 100644
index 0000000..c007a5c
--- /dev/null
+++ b/tests/main_tests.zig
@@ -0,0 +1,7 @@
+const std = @import("std");
+
+test {
+ _ = @import("config_tests.zig");
+ _ = @import("layout_tests.zig");
+ _ = @import("keybind_tests.zig");
+}