From 61cee458a1cf90862f51dbb69a8ecf490e0f20e8 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Thu, 29 Jul 2021 16:18:06 -0700 Subject: [PATCH] feat(wm): initial commit One week of blissful, in-the-zone coding, applying all of the lessons learnt from the development of yatta. --- .gitignore | 2 + Cargo.lock | 1100 ++++++++++++++++++++++ Cargo.toml | 8 + bindings/Cargo.toml | 13 + bindings/build.rs | 29 + bindings/src/lib.rs | 1 + komorebi-core/Cargo.toml | 15 + komorebi-core/src/cycle_direction.rs | 34 + komorebi-core/src/layout.rs | 193 ++++ komorebi-core/src/lib.rs | 105 +++ komorebi-core/src/operation_direction.rs | 91 ++ komorebi-core/src/rect.rs | 49 + komorebi.iml | 15 + komorebi/Cargo.lock | 555 +++++++++++ komorebi/Cargo.toml | 26 + komorebi/src/container.rs | 120 +++ komorebi/src/main.rs | 125 +++ komorebi/src/monitor.rs | 152 +++ komorebi/src/process_command.rs | 147 +++ komorebi/src/process_event.rs | 124 +++ komorebi/src/ring.rs | 46 + komorebi/src/styles.rs | 122 +++ komorebi/src/window.rs | 211 +++++ komorebi/src/window_manager.rs | 546 +++++++++++ komorebi/src/window_manager_event.rs | 90 ++ komorebi/src/windows_api.rs | 470 +++++++++ komorebi/src/windows_callbacks.rs | 125 +++ komorebi/src/winevent.rs | 173 ++++ komorebi/src/winevent_listener.rs | 107 +++ komorebi/src/workspace.rs | 537 +++++++++++ komorebic/Cargo.toml | 15 + komorebic/src/main.rs | 247 +++++ 32 files changed, 5593 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 bindings/Cargo.toml create mode 100644 bindings/build.rs create mode 100644 bindings/src/lib.rs create mode 100644 komorebi-core/Cargo.toml create mode 100644 komorebi-core/src/cycle_direction.rs create mode 100644 komorebi-core/src/layout.rs create mode 100644 komorebi-core/src/lib.rs create mode 100644 komorebi-core/src/operation_direction.rs create mode 100644 komorebi-core/src/rect.rs create mode 100644 komorebi.iml create mode 100644 komorebi/Cargo.lock create mode 100644 komorebi/Cargo.toml create mode 100644 komorebi/src/container.rs create mode 100644 komorebi/src/main.rs create mode 100644 komorebi/src/monitor.rs create mode 100644 komorebi/src/process_command.rs create mode 100644 komorebi/src/process_event.rs create mode 100644 komorebi/src/ring.rs create mode 100644 komorebi/src/styles.rs create mode 100644 komorebi/src/window.rs create mode 100644 komorebi/src/window_manager.rs create mode 100644 komorebi/src/window_manager_event.rs create mode 100644 komorebi/src/windows_api.rs create mode 100644 komorebi/src/windows_callbacks.rs create mode 100644 komorebi/src/winevent.rs create mode 100644 komorebi/src/winevent_listener.rs create mode 100644 komorebi/src/workspace.rs create mode 100644 komorebic/Cargo.toml create mode 100644 komorebic/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3a8cabc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..46e3c1ba --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1100 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bindings" +version = "0.1.0" +dependencies = [ + "windows", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "color-eyre" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "const-sha1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "ctrlc" +version = "3.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232295399409a8b7ae41276757b5a1cc21032848d42bff2352261f958b3ca29a" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221239d1d5ea86bf5d6f91c9d6bc3646ffe471b08ff9b0f91c44f115ac969d2b" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "komorebi" +version = "0.1.0" +dependencies = [ + "bindings", + "bitflags", + "color-eyre", + "crossbeam-channel", + "crossbeam-utils", + "ctrlc", + "dirs", + "eyre", + "komorebi-core", + "lazy_static", + "nanoid", + "strum", + "sysinfo", + "tracing", + "tracing-appender", + "tracing-subscriber", + "uds_windows", +] + +[[package]] +name = "komorebi-core" +version = "0.1.0" +dependencies = [ + "bindings", + "clap", + "color-eyre", + "serde", + "serde_json", + "strum", +] + +[[package]] +name = "komorebic" +version = "0.1.0" +dependencies = [ + "clap", + "color-eyre", + "dirs", + "komorebi-core", + "powershell_script", + "uds_windows", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.4", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + +[[package]] +name = "owo-colors" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "powershell_script" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232c43ca4a2aec888dbdcf0167ad05c228692624d5b75b0a775ef4065ca8b03e" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.3", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "sysinfo" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7de153d0438a648bb71e06e300e54fc641685e96af96d49b843f43172d341c" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "doc-comment", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tracing" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9965507e507f12c8901432a33e31131222abac31edd90cabbcf85cf544b7127a" +dependencies = [ + "chrono", + "crossbeam-channel", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "uds_windows" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486992108df0fe0160680af1941fe856c521be931d5a5ecccefe0de86dc47e4a" +dependencies = [ + "tempdir", + "winapi", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1c7f11b289e450f78d55dd9dc09ae91c6ae8faed980bbf3e3a4c8f166ac259" +dependencies = [ + "const-sha1", + "windows_gen", + "windows_macros", +] + +[[package]] +name = "windows_gen" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f5facfb04bc84b5fcd27018266d90ce272e11f8b91745dfdd47282e8e0607e" + +[[package]] +name = "windows_macros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c32753c378262520a4fa70c2e4389f4649e751faab2a887090567cff192d299" +dependencies = [ + "syn", + "windows_gen", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..ef752337 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] + +members = [ + "bindings", + "komorebi", + "komorebi-core", + "komorebic" +] diff --git a/bindings/Cargo.toml b/bindings/Cargo.toml new file mode 100644 index 00000000..2e995854 --- /dev/null +++ b/bindings/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bindings" +version = "0.1.0" +authors = ["Jade Iqbal"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +windows = "0.17.2" + +[build-dependencies] +windows = "0.17.2" \ No newline at end of file diff --git a/bindings/build.rs b/bindings/build.rs new file mode 100644 index 00000000..ef542e1b --- /dev/null +++ b/bindings/build.rs @@ -0,0 +1,29 @@ +fn main() { + windows::build!( + Windows::Win32::Foundation::{ + POINT, + RECT, + BOOL, + PWSTR, + HWND, + LPARAM, + }, + // error: `Windows.Win32.Graphics.Dwm.DWMWA_CLOAKED` not found in metadata + Windows::Win32::Graphics::Dwm::*, + // error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata + Windows::Win32::Graphics::Gdi::*, + Windows::Win32::System::Threading::{ + PROCESS_ACCESS_RIGHTS, + PROCESS_NAME_FORMAT, + OpenProcess, + QueryFullProcessImageNameW, + GetCurrentThreadId, + AttachThreadInput, + GetCurrentProcessId + }, + Windows::Win32::UI::KeyboardAndMouseInput::SetFocus, + Windows::Win32::UI::Accessibility::{SetWinEventHook, HWINEVENTHOOK}, + // error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata + Windows::Win32::UI::WindowsAndMessaging::*, + ); +} diff --git a/bindings/src/lib.rs b/bindings/src/lib.rs new file mode 100644 index 00000000..79157607 --- /dev/null +++ b/bindings/src/lib.rs @@ -0,0 +1 @@ +::windows::include_bindings!(); diff --git a/komorebi-core/Cargo.toml b/komorebi-core/Cargo.toml new file mode 100644 index 00000000..a7343a6f --- /dev/null +++ b/komorebi-core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "komorebi-core" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bindings = { package = "bindings", path = "../bindings" } + +color-eyre = "0.5.11" +clap = "3.0.0-beta.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +strum = { version = "0.21", features = ["derive"] } diff --git a/komorebi-core/src/cycle_direction.rs b/komorebi-core/src/cycle_direction.rs new file mode 100644 index 00000000..fd11664b --- /dev/null +++ b/komorebi-core/src/cycle_direction.rs @@ -0,0 +1,34 @@ +use clap::Clap; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +#[derive(Clap)] +pub enum CycleDirection { + Previous, + Next, +} + +impl CycleDirection { + pub fn next_idx(&self, idx: usize, len: usize) -> usize { + match self { + CycleDirection::Previous => { + if idx == 0 { + len - 1 + } else { + idx - 1 + } + } + CycleDirection::Next => { + if idx == len - 1 { + 0 + } else { + idx + 1 + } + } + } + } +} diff --git a/komorebi-core/src/layout.rs b/komorebi-core/src/layout.rs new file mode 100644 index 00000000..3bf9d40f --- /dev/null +++ b/komorebi-core/src/layout.rs @@ -0,0 +1,193 @@ +use clap::Clap; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +use crate::Rect; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +#[derive(Clap)] +pub enum Layout { + BSP, + Columns, + Rows, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +#[derive(Clap)] +pub enum LayoutFlip { + Horizontal, + Vertical, + HorizontalAndVertical, +} + +impl Layout { + pub fn calculate( + &self, + area: &Rect, + count: usize, + container_padding: Option, + layout_flip: Option, + ) -> Vec { + let mut dimensions = match self { + Layout::BSP => self.fibonacci(area, count, layout_flip), + Layout::Columns => { + let right = area.right / count as i32; + let mut left = 0; + + let mut layouts: Vec = vec![]; + for _ in 0..count { + layouts.push(Rect { + left: area.left + left, + top: area.top, + right, + bottom: area.bottom, + }); + + left += right; + } + + layouts + } + Layout::Rows => { + let bottom = area.bottom / count as i32; + let mut top = 0; + + let mut layouts: Vec = vec![]; + for _ in 0..count { + layouts.push(Rect { + left: area.left, + top: area.top + top, + right: area.right, + bottom, + }); + + top += bottom; + } + + layouts + } + }; + + dimensions + .iter_mut() + .for_each(|l| l.add_padding(container_padding)); + + dimensions + } + + pub fn fibonacci( + &self, + area: &Rect, + count: usize, + layout_flip: Option, + ) -> Vec { + let mut dimensions = vec![]; + + for _ in 0..count { + dimensions.push(Rect::default()) + } + + let mut left = area.left; + let mut top = area.top; + let mut bottom = area.bottom; + let mut right = area.right; + + for i in 0..count { + if i % 2 != 0 { + continue; + } + + let half_width = right / 2; + let half_height = bottom / 2; + + let (main_x, alt_x, new_y, alt_y); + + match layout_flip { + Some(flip) => match flip { + LayoutFlip::Horizontal => { + main_x = left + half_width; + alt_x = left; + + new_y = top + half_height; + alt_y = top; + } + LayoutFlip::Vertical => { + new_y = top; + alt_y = top + half_height; + + main_x = left; + alt_x = left + half_width; + } + LayoutFlip::HorizontalAndVertical => { + main_x = left + half_width; + alt_x = left; + new_y = top; + alt_y = top + half_height; + } + }, + None => { + main_x = left; + alt_x = left + half_width; + new_y = top + half_height; + alt_y = top; + } + } + + match count - i { + 1 => { + set_dimensions(&mut dimensions[i], left, top, right, bottom); + } + 2 => { + set_dimensions(&mut dimensions[i], main_x, top, half_width, bottom); + set_dimensions(&mut dimensions[i + 1], alt_x, top, half_width, bottom); + } + _ => { + set_dimensions(&mut dimensions[i], main_x, top, half_width, bottom); + set_dimensions( + &mut dimensions[i + 1], + alt_x, + alt_y, + half_width, + half_height, + ); + + left = alt_x; + top = new_y; + right = half_width; + bottom = half_height; + } + } + } + + dimensions + } +} + +impl Layout { + pub fn next(&mut self) { + match self { + Layout::BSP => *self = Layout::Columns, + Layout::Columns => *self = Layout::Rows, + Layout::Rows => *self = Layout::BSP, + } + } + + pub fn previous(&mut self) { + match self { + Layout::BSP => *self = Layout::Rows, + Layout::Columns => *self = Layout::BSP, + Layout::Rows => *self = Layout::Columns, + } + } +} + +fn set_dimensions(rect: &mut Rect, left: i32, top: i32, right: i32, bottom: i32) { + rect.bottom = bottom; + rect.right = right; + rect.left = left; + rect.top = top; +} diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs new file mode 100644 index 00000000..5019603a --- /dev/null +++ b/komorebi-core/src/lib.rs @@ -0,0 +1,105 @@ +use std::str::FromStr; + +use clap::Clap; +use color_eyre::Result; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +pub use cycle_direction::CycleDirection; +pub use layout::Layout; +pub use layout::LayoutFlip; +pub use operation_direction::OperationDirection; +pub use rect::Rect; + +pub mod cycle_direction; +pub mod layout; +pub mod operation_direction; +pub mod rect; + +#[derive(Clone, Debug, Serialize, Deserialize, Display)] +pub enum SocketMessage { + // Window / Container Commands + FocusWindow(OperationDirection), + MoveWindow(OperationDirection), + StackWindow(OperationDirection), + UnstackWindow, + CycleStack(CycleDirection), + MoveContainerToMonitorNumber(usize), + MoveContainerToWorkspaceNumber(usize), + Promote, + ToggleFloat, + ToggleMonocle, + // Current Workspace Commands + AdjustContainerPadding(Sizing, i32), + AdjustWorkspacePadding(Sizing, i32), + ChangeLayout(Layout), + FlipLayout(LayoutFlip), + // Monitor and Workspace Commands + Stop, + TogglePause, + Retile, + FocusMonitorNumber(usize), + FocusWorkspaceNumber(usize), + ContainerPadding(usize, usize, i32), + WorkspacePadding(usize, usize, i32), + WorkspaceName(usize, usize, String), + SetLayout(usize, usize, Layout), + // Configuration + FloatClass(String), + FloatExe(String), + FloatTitle(String), + // TODO: Add some state query commands +} + +impl SocketMessage { + pub fn as_bytes(&self) -> Result> { + Ok(serde_json::to_string(self)?.as_bytes().to_vec()) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(serde_json::from_slice(bytes)?) + } +} + +impl FromStr for SocketMessage { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +#[derive(Clap)] +pub enum Sizing { + Increase, + Decrease, +} + +impl Sizing { + pub fn adjust_by(&self, value: i32, adjustment: i32) -> i32 { + match self { + Sizing::Increase => value + adjustment, + Sizing::Decrease => { + if value > 0 && value - adjustment >= 0 { + value - adjustment + } else { + value + } + } + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +#[derive(Clap)] +pub enum ResizeEdge { + Left, + Top, + Right, + Bottom, +} diff --git a/komorebi-core/src/operation_direction.rs b/komorebi-core/src/operation_direction.rs new file mode 100644 index 00000000..96aec684 --- /dev/null +++ b/komorebi-core/src/operation_direction.rs @@ -0,0 +1,91 @@ +use clap::Clap; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +use crate::Layout; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString)] +#[strum(serialize_all = "snake_case")] +#[derive(Clap)] +pub enum OperationDirection { + Left, + Right, + Up, + Down, +} + +impl OperationDirection { + pub fn can_resize(&self, layout: Layout, idx: usize, len: usize) -> bool { + match layout { + Layout::BSP => match self { + Self::Left => len != 0 && idx != 0, + Self::Up => len > 2 && idx != 0 && idx != 1, + Self::Right => len > 1 && idx % 2 == 0 && idx != len - 1, + Self::Down => len > 2 && idx != len - 1 && idx % 2 != 0, + }, + _ => false, + } + } + + pub fn is_valid(&self, layout: Layout, idx: usize, len: usize) -> bool { + match self { + OperationDirection::Up => match layout { + Layout::BSP => len > 2 && idx != 0 && idx != 1, + Layout::Columns => false, + Layout::Rows => idx != 0, + }, + OperationDirection::Down => match layout { + Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0, + Layout::Columns => false, + Layout::Rows => idx != len - 1, + }, + OperationDirection::Left => match layout { + Layout::BSP => len > 1 && idx != 0, + Layout::Columns => idx != 0, + Layout::Rows => false, + }, + OperationDirection::Right => match layout { + Layout::BSP => len > 1 && idx % 2 == 0, + Layout::Columns => idx != len - 1, + Layout::Rows => false, + }, + } + } + + pub fn new_idx(&self, layout: Layout, idx: usize) -> usize { + match self { + OperationDirection::Up => match layout { + Layout::BSP => { + if idx % 2 == 0 { + idx - 1 + } else { + idx - 2 + } + } + Layout::Columns => unreachable!(), + Layout::Rows => idx - 1, + }, + OperationDirection::Down => match layout { + Layout::BSP | Layout::Rows => idx + 1, + Layout::Columns => unreachable!(), + }, + OperationDirection::Left => match layout { + Layout::BSP => { + if idx % 2 == 0 { + idx - 2 + } else { + idx - 1 + } + } + Layout::Columns => idx - 1, + Layout::Rows => unreachable!(), + }, + OperationDirection::Right => match layout { + Layout::BSP | Layout::Columns => idx + 1, + Layout::Rows => unreachable!(), + }, + } + } +} diff --git a/komorebi-core/src/rect.rs b/komorebi-core/src/rect.rs new file mode 100644 index 00000000..ab0fc545 --- /dev/null +++ b/komorebi-core/src/rect.rs @@ -0,0 +1,49 @@ +use bindings::Windows::Win32::Foundation::RECT; + +#[derive(Debug, Clone)] +pub struct Rect { + pub left: i32, + pub top: i32, + pub right: i32, + pub bottom: i32, +} + +impl Default for Rect { + fn default() -> Self { + Rect { + left: 0, + top: 0, + right: 0, + bottom: 0, + } + } +} + +impl From for Rect { + fn from(rect: RECT) -> Self { + Rect { + left: rect.left, + top: rect.top, + right: rect.right - rect.left, + bottom: rect.bottom - rect.top, + } + } +} + +impl Rect { + pub fn add_padding(&mut self, padding: Option) { + if let Some(padding) = padding { + self.left += padding; + self.top += padding; + self.right -= padding * 2; + self.bottom -= padding * 2; + } + } + + pub fn contains_point(&self, point: (i32, i32)) -> bool { + point.0 >= self.left + && point.0 <= self.left + self.right + && point.1 >= self.top + && point.1 <= self.top + self.bottom + } +} diff --git a/komorebi.iml b/komorebi.iml new file mode 100644 index 00000000..54547034 --- /dev/null +++ b/komorebi.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/komorebi/Cargo.lock b/komorebi/Cargo.lock new file mode 100644 index 00000000..cb740607 --- /dev/null +++ b/komorebi/Cargo.lock @@ -0,0 +1,555 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bindings" +version = "0.1.0" +dependencies = [ + "windows", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "winapi", +] + +[[package]] +name = "color-eyre" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "const-sha1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221239d1d5ea86bf5d6f91c9d6bc3646ffe471b08ff9b0f91c44f115ac969d2b" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "gimli" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "komorebi" +version = "0.1.0" +dependencies = [ + "bindings", + "bitflags", + "color-eyre", + "crossbeam-channel", + "crossbeam-utils", + "lazy_static", + "strum", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "owo-colors" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rustc-demangle" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1c7f11b289e450f78d55dd9dc09ae91c6ae8faed980bbf3e3a4c8f166ac259" +dependencies = [ + "const-sha1", + "windows_gen", + "windows_macros", +] + +[[package]] +name = "windows_gen" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f5facfb04bc84b5fcd27018266d90ce272e11f8b91745dfdd47282e8e0607e" + +[[package]] +name = "windows_macros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c32753c378262520a4fa70c2e4389f4649e751faab2a887090567cff192d299" +dependencies = [ + "syn", + "windows_gen", +] diff --git a/komorebi/Cargo.toml b/komorebi/Cargo.toml new file mode 100644 index 00000000..8e06bd6a --- /dev/null +++ b/komorebi/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "komorebi" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bindings = { package = "bindings", path = "../bindings" } +komorebi-core = { path = "../komorebi-core" } + +bitflags = "1.2.1" +color-eyre = "0.5.11" +crossbeam-channel = "0.5.1" +crossbeam-utils = "0.8.5" +ctrlc = "3" +dirs = "3" +eyre = "0.6.5" +lazy_static = "1.4.0" +nanoid = "0.4.0" +strum = { version = "0.21", features = ["derive"] } +sysinfo = "0.19" +tracing = "0.1.26" +tracing-appender = "0.1.2" +tracing-subscriber = "0.2.19" +uds_windows = "1" \ No newline at end of file diff --git a/komorebi/src/container.rs b/komorebi/src/container.rs new file mode 100644 index 00000000..5160f372 --- /dev/null +++ b/komorebi/src/container.rs @@ -0,0 +1,120 @@ +use std::collections::VecDeque; + +use nanoid::nanoid; + +use crate::ring::Ring; +use crate::window::Window; + +#[derive(Debug, Clone)] +pub struct Container { + id: String, + windows: Ring, +} + +impl Default for Container { + fn default() -> Self { + Self { + id: nanoid!(), + windows: Ring::default(), + } + } +} + +impl PartialEq for &Container { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Container { + pub fn hide(&mut self) { + for window in self.windows_mut() { + window.hide(); + } + } + + pub fn load_focused_window(&mut self) { + let focused_idx = self.focused_window_idx(); + for (i, window) in self.windows_mut().iter_mut().enumerate() { + if i == focused_idx { + window.restore(); + } else { + window.hide(); + } + } + } + + pub fn contains_window(&self, hwnd: isize) -> bool { + for window in self.windows() { + if window.hwnd == hwnd { + return true; + } + } + + false + } + + pub fn idx_for_window(&self, hwnd: isize) -> Option { + let mut idx = None; + for (i, window) in self.windows().iter().enumerate() { + if window.hwnd == hwnd { + idx = Option::from(i); + } + } + + idx + } + + pub fn remove_window_by_idx(&mut self, idx: usize) -> Option { + self.windows_mut().remove(idx) + } + + pub fn remove_focused_window(&mut self) -> Option { + let focused_idx = self.focused_window_idx(); + let window = self.remove_window_by_idx(focused_idx); + + if focused_idx != 0 { + self.focus_window(focused_idx - 1); + } + + window + } + + pub fn add_window(&mut self, window: Window) { + self.windows_mut().push_back(window); + self.focus_window(self.windows().len() - 1); + } + + pub fn focused_window(&self) -> Option<&Window> { + self.windows.focused() + } + + pub const fn focused_window_idx(&self) -> usize { + self.windows.focused_idx() + } + + pub fn focused_window_mut(&mut self) -> Option<&mut Window> { + self.windows.focused_mut() + } + + pub fn focus_window(&mut self, idx: usize) { + tracing::info!("focusing window at index: {}", idx); + self.windows.focus(idx); + } + + pub const fn windows(&self) -> &VecDeque { + self.windows.elements() + } + + pub fn windows_mut(&mut self) -> &mut VecDeque { + self.windows.elements_mut() + } + + pub fn visible_window_mut(&mut self) -> Option<&mut Window> { + self.focused_window_mut() + } + + pub const fn id(&self) -> &String { + &self.id + } +} diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs new file mode 100644 index 00000000..c494a698 --- /dev/null +++ b/komorebi/src/main.rs @@ -0,0 +1,125 @@ +#![warn(clippy::all, clippy::nursery, clippy::pedantic)] +#![allow(clippy::missing_errors_doc)] + +use std::sync::Arc; +use std::sync::Mutex; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use lazy_static::lazy_static; +use sysinfo::SystemExt; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::EnvFilter; + +use crate::process_command::listen_for_commands; +use crate::process_event::listen_for_events; +use crate::window_manager_event::WindowManagerEvent; +use crate::windows_api::WindowsApi; + +mod container; +mod monitor; +mod process_command; +mod process_event; +mod ring; +mod styles; +mod window; +mod window_manager; +mod window_manager_event; +mod windows_api; +mod windows_callbacks; +mod winevent; +mod winevent_listener; +mod workspace; + +lazy_static! { + static ref FLOAT_CLASSES: Arc>> = Arc::new(Mutex::new(vec![])); + static ref FLOAT_EXES: Arc>> = Arc::new(Mutex::new(vec![])); + static ref FLOAT_TITLES: Arc>> = Arc::new(Mutex::new(vec![])); + static ref LAYERED_EXE_WHITELIST: Arc>> = + Arc::new(Mutex::new(vec!["steam.exe".to_string()])); +} + +fn setup() -> Result { + if std::env::var("RUST_LIB_BACKTRACE").is_err() { + std::env::set_var("RUST_LIB_BACKTRACE", "1"); + } + + color_eyre::install()?; + + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "info"); + } + + let home = dirs::home_dir().context("there is no home directory")?; + let appender = tracing_appender::rolling::never(home, "komorebi.log"); + let (non_blocking, guard) = tracing_appender::non_blocking(appender); + + tracing::subscriber::set_global_default( + tracing_subscriber::fmt::Subscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .with_max_level(tracing::Level::DEBUG) + .finish() + .with( + tracing_subscriber::fmt::Layer::default() + .with_writer(non_blocking) + .with_ansi(false), + ), + )?; + + Ok(guard) +} + +fn main() -> Result<()> { + match std::env::args().count() { + 1 => { + let mut system = sysinfo::System::new_all(); + system.refresh_processes(); + + if system.process_by_name("komorebi.exe").len() > 1 { + tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one"); + std::process::exit(1); + } + + // File logging worker guard has to have an assignment in the main fn to work + let _guard = setup()?; + + let process_id = WindowsApi::current_process_id(); + WindowsApi::allow_set_foreground_window(process_id)?; + + let (outgoing, incoming): (Sender, Receiver) = + crossbeam_channel::unbounded(); + + let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing))); + winevent_listener.start(); + + let wm = Arc::new(Mutex::new(window_manager::new(Arc::new(Mutex::new( + incoming, + )))?)); + + wm.lock().unwrap().init()?; + listen_for_commands(wm.clone()); + listen_for_events(wm.clone()); + + let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1); + ctrlc::set_handler(move || { + ctrlc_sender + .send(()) + .expect("could not send signal on ctrl-c channel"); + })?; + + ctrlc_receiver + .recv() + .expect("could not receive signal on ctrl-c channel"); + + tracing::error!( + "received ctrl-c, restoring all hidden windows and terminating process" + ); + wm.lock().unwrap().restore_all_windows(); + std::process::exit(130); + } + _ => Ok(()), + } +} diff --git a/komorebi/src/monitor.rs b/komorebi/src/monitor.rs new file mode 100644 index 00000000..81c2fba8 --- /dev/null +++ b/komorebi/src/monitor.rs @@ -0,0 +1,152 @@ +use std::collections::HashMap; +use std::collections::VecDeque; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; + +use komorebi_core::Rect; + +use crate::container::Container; +use crate::ring::Ring; +use crate::workspace::Workspace; + +#[derive(Debug, Clone)] +pub struct Monitor { + id: isize, + monitor_size: Rect, + work_area_size: Rect, + workspaces: Ring, + workspace_names: HashMap, +} + +pub fn new(id: isize, monitor_size: Rect, work_area_size: Rect) -> Monitor { + Monitor { + id, + monitor_size, + work_area_size, + workspaces: Ring::default(), + workspace_names: HashMap::default(), + } +} + +impl Monitor { + pub fn load_focused_workspace(&mut self) { + let focused_idx = self.focused_workspace_idx(); + for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() { + if i == focused_idx { + workspace.restore(); + } else { + workspace.hide(); + } + } + } + + pub fn add_container(&mut self, container: Container) -> Result<()> { + let workspace = self + .focused_workspace_mut() + .context("there is no workspace")?; + + workspace.add_container(container); + + Ok(()) + } + + pub fn move_container_to_workspace( + &mut self, + target_workspace_idx: usize, + follow: bool, + ) -> Result<()> { + let container = self + .focused_workspace_mut() + .context("there is no workspace")? + .remove_focused_container() + .context("there is no container")?; + + let workspaces = self.workspaces_mut(); + + let target_workspace = match workspaces.get_mut(target_workspace_idx) { + None => { + workspaces.resize(target_workspace_idx + 1, Workspace::default()); + workspaces.get_mut(target_workspace_idx).unwrap() + } + Some(workspace) => workspace, + }; + + target_workspace.add_container(container); + + if follow { + self.focus_workspace(target_workspace_idx)?; + } + + Ok(()) + } + + pub fn focused_workspace(&self) -> Option<&Workspace> { + self.workspaces.focused() + } + + pub const fn focused_workspace_idx(&self) -> usize { + self.workspaces.focused_idx() + } + + pub fn focused_workspace_mut(&mut self) -> Option<&mut Workspace> { + self.workspaces.focused_mut() + } + + pub fn focus_workspace(&mut self, idx: usize) -> Result<()> { + { + let workspaces = self.workspaces_mut(); + + tracing::info!("focusing workspace at index: {}", idx); + if workspaces.get(idx).is_none() { + workspaces.resize(idx + 1, Workspace::default()); + } + + self.workspaces.focus(idx); + } + + // Always set the latest known name when creating the workspace for the first time + { + let name = { self.workspace_names.get(&idx).cloned() }; + if name.is_some() { + self.workspaces_mut() + .get_mut(idx) + .context("there is no workspace")? + .set_name(name); + } + } + + Ok(()) + } + + pub fn update_focused_workspace(&mut self) -> Result<()> { + tracing::info!("updating workspace: {}", self.focused_workspace_idx()); + let work_area = self.work_area_size().clone(); + + self.focused_workspace_mut() + .context("there is no workspace")? + .update(&work_area)?; + + Ok(()) + } + + pub const fn workspaces(&self) -> &VecDeque { + self.workspaces.elements() + } + + pub fn workspaces_mut(&mut self) -> &mut VecDeque { + self.workspaces.elements_mut() + } + + pub fn workspace_names_mut(&mut self) -> &mut HashMap { + &mut self.workspace_names + } + + pub const fn id(&self) -> isize { + self.id + } + + pub const fn work_area_size(&self) -> &Rect { + &self.work_area_size + } +} diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs new file mode 100644 index 00000000..6933411d --- /dev/null +++ b/komorebi/src/process_command.rs @@ -0,0 +1,147 @@ +use std::io::BufRead; +use std::io::BufReader; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::thread; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; +use uds_windows::UnixStream; + +use komorebi_core::SocketMessage; + +use crate::window_manager::WindowManager; +use crate::FLOAT_CLASSES; +use crate::FLOAT_EXES; +use crate::FLOAT_TITLES; + +pub fn listen_for_commands(wm: Arc>) { + let listener = wm + .lock() + .unwrap() + .command_listener + .try_clone() + .expect("could not clone unix listener"); + + thread::spawn(move || { + tracing::info!("listening for commands"); + for client in listener.incoming() { + match client { + Ok(stream) => match wm.lock().unwrap().process_command(stream) { + Ok(()) => tracing::info!("command processed"), + Err(error) => tracing::error!("{}", error), + }, + Err(error) => { + tracing::error!("{}", error); + break; + } + } + } + }); +} + +impl WindowManager { + pub fn process_command(&mut self, stream: UnixStream) -> Result<()> { + let stream = BufReader::new(stream); + for line in stream.lines() { + let message = SocketMessage::from_str(&line?)?; + + if self.is_paused { + if let SocketMessage::TogglePause = message { + tracing::info!("resuming window management"); + self.is_paused = !self.is_paused; + return Ok(()); + } + + tracing::info!("ignoring commands while paused"); + return Ok(()); + } + + tracing::info!("processing command: {}", &message); + match message { + SocketMessage::Promote => self.promote_container_to_front()?, + SocketMessage::FocusWindow(direction) => { + self.focus_container_in_direction(direction)?; + } + SocketMessage::MoveWindow(direction) => { + self.move_container_in_direction(direction)?; + } + SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?, + SocketMessage::UnstackWindow => self.remove_window_from_container()?, + SocketMessage::CycleStack(direction) => { + self.cycle_container_window_in_direction(direction)?; + } + SocketMessage::ToggleFloat => self.toggle_float()?, + SocketMessage::ToggleMonocle => self.toggle_monocle()?, + SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => { + self.set_container_padding(monitor_idx, workspace_idx, size)?; + } + SocketMessage::WorkspacePadding(monitor_idx, workspace_idx, size) => { + self.set_workspace_padding(monitor_idx, workspace_idx, size)?; + } + SocketMessage::FloatClass(target) => { + let mut float_classes = FLOAT_CLASSES.lock().unwrap(); + if !float_classes.contains(&target) { + float_classes.push(target); + } + } + SocketMessage::FloatExe(target) => { + let mut float_exes = FLOAT_EXES.lock().unwrap(); + if !float_exes.contains(&target) { + float_exes.push(target); + } + } + SocketMessage::FloatTitle(target) => { + let mut float_titles = FLOAT_TITLES.lock().unwrap(); + if !float_titles.contains(&target) { + float_titles.push(target); + } + } + SocketMessage::AdjustContainerPadding(sizing, adjustment) => { + self.adjust_container_padding(sizing, adjustment)?; + } + SocketMessage::AdjustWorkspacePadding(sizing, adjustment) => { + self.adjust_workspace_padding(sizing, adjustment)?; + } + SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => { + self.move_container_to_workspace(workspace_idx, true)?; + } + SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => { + self.move_container_to_monitor(monitor_idx, true)?; + } + SocketMessage::TogglePause => self.is_paused = !self.is_paused, + SocketMessage::FocusMonitorNumber(monitor_idx) => { + self.focus_monitor(monitor_idx)?; + self.update_focused_workspace(true)?; + } + SocketMessage::Retile => { + for monitor in self.monitors_mut() { + let work_area = monitor.work_area_size().clone(); + monitor + .focused_workspace_mut() + .context("there is no workspace")? + .update(&work_area)?; + } + } + SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?, + SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?, + SocketMessage::SetLayout(monitor_idx, workspace_idx, layout) => { + self.set_workspace_layout(monitor_idx, workspace_idx, layout)?; + } + SocketMessage::FocusWorkspaceNumber(workspace_idx) => { + self.focus_workspace(workspace_idx)?; + } + SocketMessage::Stop => { + tracing::error!("received stop command, restoring all hidden windows and terminating process"); + self.restore_all_windows(); + std::process::exit(0) + } + SocketMessage::WorkspaceName(monitor_idx, workspace_idx, name) => { + self.set_workspace_name(monitor_idx, workspace_idx, name)?; + } + } + } + + Ok(()) + } +} diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs new file mode 100644 index 00000000..eebde1de --- /dev/null +++ b/komorebi/src/process_event.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; +use crossbeam_channel::select; + +use crate::window_manager::WindowManager; +use crate::window_manager_event::WindowManagerEvent; + +pub fn listen_for_events(wm: Arc>) { + let receiver = wm.lock().unwrap().incoming_events.lock().unwrap().clone(); + + thread::spawn(move || { + tracing::info!("listening for events"); + loop { + select! { + recv(receiver) -> mut maybe_event => { + if let Ok(event) = maybe_event.as_mut() { + match wm.lock().unwrap().process_event(event) { + Ok(()) => {}, + Err(error) => tracing::error!("{}", error) + } + } + } + } + } + }); +} + +impl WindowManager { + pub fn process_event(&mut self, event: &mut WindowManagerEvent) -> Result<()> { + if self.is_paused { + tracing::info!("ignoring events while paused"); + return Ok(()); + } + + // Make sure we have the most recently focused monitor from any event + match event { + WindowManagerEvent::FocusChange(_, window) + | WindowManagerEvent::Show(_, window) + | WindowManagerEvent::MoveResizeStart(_, window) + | WindowManagerEvent::MoveResizeEnd(_, window) => { + let monitor_idx = self + .monitor_idx_from_window(window) + .context("there is no monitor associated with this window, it may have already been destroyed")?; + + self.focus_monitor(monitor_idx)?; + } + _ => {} + } + + for (i, monitor) in self.monitors_mut().iter_mut().enumerate() { + let work_area = monitor.work_area_size().clone(); + for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() { + let reaped_orphans = workspace.reap_orphans()?; + if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 { + workspace.update(&work_area)?; + tracing::info!( + "reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}", + reaped_orphans.0, + reaped_orphans.1, + i, + j + ); + } + } + } + + if matches!(event, WindowManagerEvent::MouseCapture(_, _)) { + tracing::trace!("only reaping orphans for mouse capture event"); + return Ok(()); + } + + tracing::info!("processing event: {}", event); + + match event { + WindowManagerEvent::Minimize(_, window) | WindowManagerEvent::Destroy(_, window) => { + self.focused_workspace_mut()?.remove_window(window.hwnd)?; + self.update_focused_workspace(false)?; + } + + WindowManagerEvent::Hide(_, window) => { + // explorer.exe is always returns true even if it's running in the background, + // but we want to be able to handle the event when File Explorer windows close properly + if !window.is_window() || window.exe()? == "explorer.exe" { + self.focused_workspace_mut()?.remove_window(window.hwnd)?; + self.update_focused_workspace(false)?; + } + } + WindowManagerEvent::FocusChange(_, window) => self + .focused_workspace_mut()? + .focus_container_by_window(window.hwnd)?, + WindowManagerEvent::Show(_, window) => { + let workspace = self.focused_workspace_mut()?; + + if workspace.containers().is_empty() || !workspace.contains_window(window.hwnd) { + workspace.new_container_for_window(*window); + self.update_focused_workspace(false)?; + } + } + WindowManagerEvent::MoveResizeStart(_, _window) => { + // TODO: Implement dragging resize (one day) + } + WindowManagerEvent::MoveResizeEnd(_, _window) => { + let workspace = self.focused_workspace_mut()?; + let focused_idx = workspace.focused_container_idx(); + + match workspace.container_idx_from_current_point() { + Some(target_idx) => { + workspace.swap_containers(focused_idx, target_idx); + self.update_focused_workspace(false)?; + } + None => self.update_focused_workspace(true)?, + } + } + WindowManagerEvent::MouseCapture(..) => {} + }; + + tracing::info!("finished processing event: {}", event); + Ok(()) + } +} diff --git a/komorebi/src/ring.rs b/komorebi/src/ring.rs new file mode 100644 index 00000000..a75ad8d9 --- /dev/null +++ b/komorebi/src/ring.rs @@ -0,0 +1,46 @@ +use std::collections::VecDeque; + +#[derive(Debug, Clone)] +pub struct Ring { + elements: VecDeque, + focused: usize, +} + +impl Default for Ring { + fn default() -> Self { + Self { + elements: VecDeque::default(), + focused: 0, + } + } +} + +impl Ring { + pub const fn elements(&self) -> &VecDeque { + &self.elements + } + + pub fn elements_mut(&mut self) -> &mut VecDeque { + &mut self.elements + } + + pub fn focus(&mut self, idx: usize) { + self.focused = idx; + } + + pub fn focused(&self) -> Option<&T> { + self.elements.get(self.focused) + } + + pub const fn focused_idx(&self) -> usize { + self.focused + } + + pub fn focused_mut(&mut self) -> Option<&mut T> { + self.elements.get_mut(self.focused) + } + + pub fn swap(&mut self, i: usize, j: usize) { + self.elements.swap(i, j); + } +} diff --git a/komorebi/src/styles.rs b/komorebi/src/styles.rs new file mode 100644 index 00000000..60a614cc --- /dev/null +++ b/komorebi/src/styles.rs @@ -0,0 +1,122 @@ +use bitflags::bitflags; + +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_BORDER; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CAPTION; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILD; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILDWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPCHILDREN; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPSIBLINGS; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DISABLED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DLGFRAME; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_ACCEPTFILES; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_APPWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CLIENTEDGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_COMPOSITED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTEXTHELP; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTROLPARENT; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_DLGMODALFRAME; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYOUTRTL; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFT; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFTSCROLLBAR; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LTRREADING; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_MDICHILD; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOINHERITLAYOUT; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOPARENTNOTIFY; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOREDIRECTIONBITMAP; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_OVERLAPPEDWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_PALETTEWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHT; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHTSCROLLBAR; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RTLREADING; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_STATICEDGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_WINDOWEDGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_GROUP; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_HSCROLL; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_ICONIC; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPEDWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUP; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUPWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SIZEBOX; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TABSTOP; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_THICKFRAME; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILEDWINDOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL; + +bitflags! { + #[derive(Default)] + pub struct GwlStyle: u32 { + const BORDER = WS_BORDER.0; + const CAPTION = WS_CAPTION.0; + const CHILD = WS_CHILD.0; + const CHILDWINDOW = WS_CHILDWINDOW.0; + const CLIPCHILDREN = WS_CLIPCHILDREN.0; + const CLIPSIBLINGS = WS_CLIPSIBLINGS.0; + const DISABLED = WS_DISABLED.0; + const DLGFRAME = WS_DLGFRAME.0; + const GROUP = WS_GROUP.0; + const HSCROLL = WS_HSCROLL.0; + const ICONIC = WS_ICONIC.0; + const MAXIMIZE = WS_MAXIMIZE.0; + const MAXIMIZEBOX = WS_MAXIMIZEBOX.0; + const MINIMIZE = WS_MINIMIZE.0; + const MINIMIZEBOX = WS_MINIMIZEBOX.0; + const OVERLAPPED = WS_OVERLAPPED.0; + const OVERLAPPEDWINDOW = WS_OVERLAPPEDWINDOW.0; + const POPUP = WS_POPUP.0; + const POPUPWINDOW = WS_POPUPWINDOW.0; + const SIZEBOX = WS_SIZEBOX.0; + const SYSMENU = WS_SYSMENU.0; + const TABSTOP = WS_TABSTOP.0; + const THICKFRAME = WS_THICKFRAME.0; + const TILED = WS_TILED.0; + const TILEDWINDOW = WS_TILEDWINDOW.0; + const VISIBLE = WS_VISIBLE.0; + const VSCROLL = WS_VSCROLL.0; + } +} + +bitflags! { + #[derive(Default)] + pub struct GwlExStyle: u32 { + const ACCEPTFILES = WS_EX_ACCEPTFILES.0; + const APPWINDOW = WS_EX_APPWINDOW.0; + const CLIENTEDGE = WS_EX_CLIENTEDGE.0; + const COMPOSITED = WS_EX_COMPOSITED.0; + const CONTEXTHELP = WS_EX_CONTEXTHELP.0; + const CONTROLPARENT = WS_EX_CONTROLPARENT.0; + const DLGMODALFRAME = WS_EX_DLGMODALFRAME.0; + const LAYERED = WS_EX_LAYERED.0; + const LAYOUTRTL = WS_EX_LAYOUTRTL.0; + const LEFT = WS_EX_LEFT.0; + const LEFTSCROLLBAR = WS_EX_LEFTSCROLLBAR.0; + const LTRREADING = WS_EX_LTRREADING.0; + const MDICHILD = WS_EX_MDICHILD.0; + const NOACTIVATE = WS_EX_NOACTIVATE.0; + const NOINHERITLAYOUT = WS_EX_NOINHERITLAYOUT.0; + const NOPARENTNOTIFY = WS_EX_NOPARENTNOTIFY.0; + const NOREDIRECTIONBITMAP = WS_EX_NOREDIRECTIONBITMAP.0; + const OVERLAPPEDWINDOW = WS_EX_OVERLAPPEDWINDOW.0; + const PALETTEWINDOW = WS_EX_PALETTEWINDOW.0; + const RIGHT = WS_EX_RIGHT.0; + const RIGHTSCROLLBAR = WS_EX_RIGHTSCROLLBAR.0; + const RTLREADING = WS_EX_RTLREADING.0; + const STATICEDGE = WS_EX_STATICEDGE.0; + const TOOLWINDOW = WS_EX_TOOLWINDOW.0; + const TOPMOST = WS_EX_TOPMOST.0; + const TRANSPARENT = WS_EX_TRANSPARENT.0; + const WINDOWEDGE = WS_EX_WINDOWEDGE.0; + } +} diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs new file mode 100644 index 00000000..d3c3bc7a --- /dev/null +++ b/komorebi/src/window.rs @@ -0,0 +1,211 @@ +use std::convert::TryFrom; +use std::fmt::Display; +use std::fmt::Formatter; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; + +use bindings::Windows::Win32::Foundation::HWND; +use komorebi_core::Rect; + +use crate::styles::GwlExStyle; +use crate::styles::GwlStyle; +use crate::window_manager_event::WindowManagerEvent; +use crate::windows_api::WindowsApi; +use crate::FLOAT_CLASSES; +use crate::FLOAT_EXES; +use crate::FLOAT_TITLES; +use crate::LAYERED_EXE_WHITELIST; + +#[derive(Debug, Clone, Copy)] +pub struct Window { + pub(crate) hwnd: isize, + pub(crate) original_style: GwlStyle, +} + +impl Display for Window { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut display = format!("(hwnd: {}", self.hwnd); + + if let Ok(title) = self.title() { + display.push_str(&format!(", title: {}", title)); + } + + if let Ok(exe) = self.exe() { + display.push_str(&format!(", exe: {}", exe)); + } + + if let Ok(class) = self.class() { + display.push_str(&format!(", class: {}", class)); + } + + display.push(')'); + + write!(f, "{}", display) + } +} + +impl Window { + pub const fn hwnd(&self) -> HWND { + HWND(self.hwnd) + } + + pub fn set_position(&mut self, layout: &Rect) -> Result<()> { + WindowsApi::set_window_pos(self.hwnd(), layout) + } + + pub fn hide(&self) { + WindowsApi::hide_window(self.hwnd()); + } + + pub fn restore(&self) { + WindowsApi::restore_window(self.hwnd()); + } + + pub fn focus(&self) -> Result<()> { + // Attach komorebi thread to Window thread + let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd()); + let current_thread_id = WindowsApi::current_thread_id(); + WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true)?; + + // Raise Window to foreground + WindowsApi::set_foreground_window(self.hwnd())?; + + // Center cursor in Window + WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?; + + // This isn't really needed when the above command works as expected via AHK + WindowsApi::set_focus(self.hwnd()) + } + + pub fn update_style(&self, style: GwlStyle) -> Result<()> { + WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?) + } + + pub fn restore_style(&self) -> Result<()> { + self.update_style(self.original_style) + } + + pub fn remove_border(&self) -> Result<()> { + let mut style = self.style()?; + style.remove(GwlStyle::BORDER); + self.update_style(style) + } + + pub fn add_border(&self) -> Result<()> { + let mut style = self.style()?; + style.insert(GwlStyle::BORDER); + self.update_style(style) + } + + pub fn remove_padding_and_title_bar(&self) -> Result<()> { + let mut style = self.style()?; + style.remove(GwlStyle::THICKFRAME); + style.remove(GwlStyle::CAPTION); + self.update_style(style) + } + + pub fn add_padding_padding_and_title_bar(&self) -> Result<()> { + let mut style = self.style()?; + style.insert(GwlStyle::THICKFRAME); + style.insert(GwlStyle::CAPTION); + self.update_style(style) + } + + pub fn style(&self) -> Result { + let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?; + GwlStyle::from_bits(bits).context("there is no gwl style") + } + + pub fn ex_style(&self) -> Result { + let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?; + GwlExStyle::from_bits(bits).context("there is no gwl style") + } + + pub fn title(&self) -> Result { + WindowsApi::window_text_w(self.hwnd()) + } + + pub fn exe(&self) -> Result { + let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd()); + WindowsApi::exe(WindowsApi::process_handle(process_id)?) + } + + pub fn class(&self) -> Result { + WindowsApi::real_window_class_w(self.hwnd()) + } + + pub fn is_cloaked(&self) -> Result { + WindowsApi::is_window_cloaked(self.hwnd()) + } + + pub fn is_window(self) -> bool { + WindowsApi::is_window(self.hwnd()) + } + + pub fn should_manage(&self, event: Option) -> Result { + let classes = FLOAT_CLASSES.lock().unwrap(); + let exes = FLOAT_EXES.lock().unwrap(); + let titles = FLOAT_TITLES.lock().unwrap(); + + if self.title().is_err() { + return Ok(false); + } + + let is_cloaked = self.is_cloaked()?; + + let mut allow_cloaked = false; + if let Some(WindowManagerEvent::Hide(_, _)) = event { + allow_cloaked = true; + } + + match (allow_cloaked, is_cloaked) { + // If allowing cloaked windows, we don't need to check the cloaked status + (true, _) | + // If not allowing cloaked windows, we need to ensure the window is not cloaked + (false, false) => { + if let (Ok(title), Ok(exe_name)) = (self.title(), self.exe()) { + if titles.contains(&title) { + return Ok(false); + } + + if exes.contains(&exe_name) { + return Ok(false); + } + + if let Ok(class) = self.class() { + if classes.contains(&class) { + return Ok(false); + } + } + + let allow_layered = LAYERED_EXE_WHITELIST.lock().unwrap().contains(&exe_name); + + let style = self.style()?; + let ex_style = self.ex_style()?; + + if style.contains(GwlStyle::CAPTION) + && ex_style.contains(GwlExStyle::WINDOWEDGE) + && !ex_style.contains(GwlExStyle::DLGMODALFRAME) + // Get a lot of dupe events coming through that make the redrawing go crazy + // on FocusChange events if I don't filter out this one. But, if we are + // allowing a specific layered window on the whitelist (like Steam), it should + // pass this check + && (allow_layered || !ex_style.contains(GwlExStyle::LAYERED)) + { + Ok(true) + } else { + if let Some(event) = event { + tracing::debug!("ignoring window: {} (event: {})", self, event); + } + + Ok(false) + } + } else { + Ok(false) + } + } + _ => Ok(false), + } + } +} diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs new file mode 100644 index 00000000..96873cb4 --- /dev/null +++ b/komorebi/src/window_manager.rs @@ -0,0 +1,546 @@ +use std::collections::VecDeque; +use std::io::ErrorKind; +use std::sync::Arc; +use std::sync::Mutex; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; +use crossbeam_channel::Receiver; +use uds_windows::UnixListener; + +use komorebi_core::CycleDirection; +use komorebi_core::Layout; +use komorebi_core::LayoutFlip; +use komorebi_core::OperationDirection; +use komorebi_core::Rect; +use komorebi_core::Sizing; + +use crate::container::Container; +use crate::monitor::Monitor; +use crate::ring::Ring; +use crate::window::Window; +use crate::window_manager_event::WindowManagerEvent; +use crate::windows_api::WindowsApi; +use crate::workspace::Workspace; + +#[derive(Debug)] +pub struct WindowManager { + pub monitors: Ring, + pub incoming_events: Arc>>, + pub command_listener: UnixListener, + pub is_paused: bool, +} + +pub fn new(incoming: Arc>>) -> Result { + let home = dirs::home_dir().context("there is no home directory")?; + let mut socket = home; + socket.push("komorebi.sock"); + let socket = socket.as_path(); + + match std::fs::remove_file(&socket) { + Ok(_) => {} + Err(error) => match error.kind() { + // Doing this because ::exists() doesn't work reliably on Windows via IntelliJ + ErrorKind::NotFound => {} + _ => { + return Err(error.into()); + } + }, + }; + + let listener = UnixListener::bind(&socket)?; + + Ok(WindowManager { + monitors: Ring::default(), + incoming_events: incoming, + command_listener: listener, + is_paused: false, + }) +} + +impl WindowManager { + pub fn init(&mut self) -> Result<()> { + tracing::info!("initialising"); + WindowsApi::load_monitor_information(&mut self.monitors)?; + WindowsApi::load_workspace_information(&mut self.monitors)?; + self.update_focused_workspace(false) + } + + pub fn update_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> { + tracing::info!("updating monitor: {}", self.focused_monitor_idx()); + + self.focused_monitor_mut() + .context("there is no monitor")? + .update_focused_workspace()?; + + if mouse_follows_focus { + self.focused_window_mut()?.focus()?; + } + + Ok(()) + } + + pub fn restore_all_windows(&mut self) { + for monitor in self.monitors_mut() { + for workspace in monitor.workspaces_mut() { + for containers in workspace.containers_mut() { + for window in containers.windows_mut() { + window.restore(); + } + } + } + } + } + + pub fn move_container_to_monitor(&mut self, idx: usize, follow: bool) -> Result<()> { + let monitor = self.focused_monitor_mut().context("there is no monitor")?; + let container = monitor + .focused_workspace_mut() + .context("there is no workspace")? + .remove_focused_container() + .context("there is no container")?; + + let target_monitor = self + .monitors_mut() + .get_mut(idx) + .context("there is no monitor")?; + + target_monitor.add_container(container)?; + target_monitor.load_focused_workspace(); + + if follow { + self.focus_monitor(idx)?; + } + + self.update_focused_workspace(true) + } + + pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> { + let monitor = self.focused_monitor_mut().context("there is no monitor")?; + monitor.move_container_to_workspace(idx, follow)?; + monitor.load_focused_workspace(); + self.update_focused_workspace(true) + } + + pub fn focus_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + + let new_idx = workspace + .new_idx_for_direction(direction) + .context("this is not a valid direction from the current position")?; + + workspace.focus_container(new_idx); + self.focused_window_mut()?.focus()?; + + Ok(()) + } + + pub fn move_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + + let current_idx = workspace.focused_container_idx(); + let new_idx = workspace + .new_idx_for_direction(direction) + .context("this is not a valid direction from the current position")?; + + workspace.swap_containers(current_idx, new_idx); + workspace.focus_container(new_idx); + self.update_focused_workspace(true) + } + + pub fn cycle_container_window_in_direction(&mut self, direction: CycleDirection) -> Result<()> { + let container = self.focused_container_mut()?; + + if container.windows().len() == 1 { + return Err(eyre::anyhow!("there is only one window in this container")); + } + + let current_idx = container.focused_window_idx(); + let next_idx = direction.next_idx(current_idx, container.windows().len()); + + container.focus_window(next_idx); + container.load_focused_window(); + + self.update_focused_workspace(true) + } + + pub fn add_window_to_container(&mut self, direction: OperationDirection) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + let current_container_idx = workspace.focused_container_idx(); + + let is_valid = direction.is_valid( + workspace.layout(), + workspace.focused_container_idx(), + workspace.containers_mut().len(), + ); + + if is_valid { + let new_idx = workspace + .new_idx_for_direction(direction) + .context("this is not a valid direction from the current position")?; + + let adjusted_new_index = if new_idx > current_container_idx { + new_idx - 1 + } else { + new_idx + }; + + workspace.move_window_to_container(adjusted_new_index)?; + self.update_focused_workspace(true)?; + } + + Ok(()) + } + + pub fn promote_container_to_front(&mut self) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + workspace.promote_container()?; + self.update_focused_workspace(true) + } + + pub fn remove_window_from_container(&mut self) -> Result<()> { + if self.focused_container()?.windows().len() == 1 { + return Err(eyre::anyhow!("a container must have at least one window")); + } + + let workspace = self.focused_workspace_mut()?; + + workspace.new_container_for_focused_window()?; + self.update_focused_workspace(true) + } + + pub fn toggle_float(&mut self) -> Result<()> { + let hwnd = WindowsApi::foreground_window()?; + let workspace = self.focused_workspace_mut()?; + + let mut is_floating_window = false; + + for window in workspace.floating_windows() { + if window.hwnd == hwnd { + is_floating_window = true; + } + } + + if is_floating_window { + self.unfloat_window()?; + } else { + self.float_window()?; + } + + self.update_focused_workspace(true) + } + + pub fn float_window(&mut self) -> Result<()> { + let work_area = self.focused_monitor_work_area()?; + + let workspace = self.focused_workspace_mut()?; + workspace.new_floating_window()?; + + let window = workspace + .floating_windows_mut() + .last_mut() + .context("there is no floating window")?; + + let half_width = work_area.right / 2; + let half_weight = work_area.bottom / 2; + + let center = Rect { + left: work_area.left + ((work_area.right - half_width) / 2), + top: work_area.top + ((work_area.bottom - half_weight) / 2), + right: half_width, + bottom: half_weight, + }; + + window.set_position(¢er)?; + window.focus()?; + + Ok(()) + } + + pub fn unfloat_window(&mut self) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + workspace.new_container_for_floating_window() + } + + pub fn toggle_monocle(&mut self) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + + match workspace.monocle_container() { + None => self.monocle_on()?, + Some(_) => self.monocle_off()?, + } + + self.update_focused_workspace(true) + } + + pub fn monocle_on(&mut self) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + workspace.new_monocle_container() + } + + pub fn monocle_off(&mut self) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + workspace.reintegrate_monocle_container() + } + + pub fn flip_layout(&mut self, layout_flip: LayoutFlip) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + + #[allow(clippy::match_same_arms)] + match workspace.layout_flip() { + None => workspace.set_layout_flip(Option::from(layout_flip)), + Some(current_layout_flip) => { + match current_layout_flip { + LayoutFlip::Horizontal => match layout_flip { + LayoutFlip::Horizontal => workspace.set_layout_flip(None), + LayoutFlip::Vertical => workspace + .set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)), + LayoutFlip::HorizontalAndVertical => workspace + .set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)), + }, + LayoutFlip::Vertical => match layout_flip { + LayoutFlip::Horizontal => workspace + .set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)), + LayoutFlip::Vertical => workspace.set_layout_flip(None), + LayoutFlip::HorizontalAndVertical => workspace + .set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)), + }, + LayoutFlip::HorizontalAndVertical => { + match layout_flip { + LayoutFlip::Horizontal => { + workspace.set_layout_flip(Option::from(LayoutFlip::Vertical)); + } + LayoutFlip::Vertical => { + workspace.set_layout_flip(Option::from(LayoutFlip::Horizontal)); + } + LayoutFlip::HorizontalAndVertical => workspace.set_layout_flip(None), + }; + } + } + } + } + + self.update_focused_workspace(false) + } + + pub fn change_workspace_layout(&mut self, layout: Layout) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + workspace.set_layout(layout); + self.update_focused_workspace(false) + } + + pub fn adjust_workspace_padding(&mut self, sizing: Sizing, adjustment: i32) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + + let padding = workspace + .workspace_padding() + .context("there is no workspace padding")?; + + workspace.set_workspace_padding(Option::from(sizing.adjust_by(padding, adjustment))); + + self.update_focused_workspace(false) + } + + pub fn adjust_container_padding(&mut self, sizing: Sizing, adjustment: i32) -> Result<()> { + let workspace = self.focused_workspace_mut()?; + + let padding = workspace + .container_padding() + .context("there is no container padding")?; + + workspace.set_container_padding(Option::from(sizing.adjust_by(padding, adjustment))); + + self.update_focused_workspace(false) + } + + pub fn set_workspace_layout( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + layout: Layout, + ) -> Result<()> { + let focused_monitor_idx = self.focused_monitor_idx(); + + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .context("there is no monitor")?; + + let work_area = monitor.work_area_size().clone(); + let focused_workspace_idx = monitor.focused_workspace_idx(); + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .context("there is no monitor")?; + + workspace.set_layout(layout); + + // If this is the focused workspace on a non-focused screen, let's update it + if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx { + workspace.update(&work_area)?; + Ok(()) + } else { + Ok(self.update_focused_workspace(false)?) + } + } + + pub fn set_workspace_padding( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + size: i32, + ) -> Result<()> { + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .context("there is no monitor")?; + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .context("there is no monitor")?; + + workspace.set_workspace_padding(Option::from(size)); + + self.update_focused_workspace(false) + } + + pub fn set_workspace_name( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + name: String, + ) -> Result<()> { + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .context("there is no monitor")?; + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .context("there is no monitor")?; + + workspace.set_name(Option::from(name.clone())); + monitor.workspace_names_mut().insert(workspace_idx, name); + + Ok(()) + } + + pub fn set_container_padding( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + size: i32, + ) -> Result<()> { + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .context("there is no monitor")?; + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .context("there is no monitor")?; + + workspace.set_container_padding(Option::from(size)); + + self.update_focused_workspace(false) + } +} + +impl WindowManager { + pub const fn monitors(&self) -> &VecDeque { + self.monitors.elements() + } + + pub fn monitors_mut(&mut self) -> &mut VecDeque { + self.monitors.elements_mut() + } + + pub fn focused_monitor(&self) -> Option<&Monitor> { + self.monitors.focused() + } + + pub const fn focused_monitor_idx(&self) -> usize { + self.monitors.focused_idx() + } + + pub fn focused_monitor_mut(&mut self) -> Option<&mut Monitor> { + self.monitors.focused_mut() + } + + pub fn focused_monitor_work_area(&self) -> Result { + Ok(self + .focused_monitor() + .context("there is no monitor")? + .work_area_size() + .clone()) + } + + pub fn focus_monitor(&mut self, idx: usize) -> Result<()> { + if self.monitors().get(idx).is_some() { + self.monitors.focus(idx); + } else { + return Err(eyre::anyhow!("this is not a valid monitor index")); + } + + Ok(()) + } + + pub fn monitor_idx_from_window(&mut self, window: &Window) -> Option { + let hmonitor = WindowsApi::monitor_from_window(window.hwnd()); + + for (i, monitor) in self.monitors().iter().enumerate() { + if monitor.id() == hmonitor { + return Option::from(i); + } + } + + None + } + + pub fn focused_workspace(&self) -> Result<&Workspace> { + self.focused_monitor() + .context("there is no monitor")? + .focused_workspace() + .context("there is no workspace") + } + + pub fn focused_workspace_mut(&mut self) -> Result<&mut Workspace> { + self.focused_monitor_mut() + .context("there is no monitor")? + .focused_workspace_mut() + .context("there is no workspace") + } + + pub fn focus_workspace(&mut self, idx: usize) -> Result<()> { + let monitor = self + .focused_monitor_mut() + .context("there is no workspace")?; + + monitor.focus_workspace(idx)?; + monitor.load_focused_workspace(); + + self.update_focused_workspace(true) + } + + pub fn focused_container(&self) -> Result<&Container> { + self.focused_workspace()? + .focused_container() + .context("there is no container") + } + + pub fn focused_container_mut(&mut self) -> Result<&mut Container> { + self.focused_workspace_mut()? + .focused_container_mut() + .context("there is no container") + } + + fn focused_window_mut(&mut self) -> Result<&mut Window> { + self.focused_container_mut()? + .focused_window_mut() + .context("there is no window") + } +} diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs new file mode 100644 index 00000000..345c05cc --- /dev/null +++ b/komorebi/src/window_manager_event.rs @@ -0,0 +1,90 @@ +use std::fmt::Display; +use std::fmt::Formatter; + +use crate::window::Window; +use crate::winevent::WinEvent; + +#[derive(Debug, Copy, Clone)] +pub enum WindowManagerEvent { + Destroy(WinEvent, Window), + FocusChange(WinEvent, Window), + Hide(WinEvent, Window), + Minimize(WinEvent, Window), + Show(WinEvent, Window), + MoveResizeStart(WinEvent, Window), + MoveResizeEnd(WinEvent, Window), + MouseCapture(WinEvent, Window), +} + +impl Display for WindowManagerEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + WindowManagerEvent::Destroy(winevent, window) => { + write!(f, "Destroy (WinEvent: {}, Window: {})", winevent, window) + } + WindowManagerEvent::FocusChange(winevent, window) => { + write!( + f, + "FocusChange (WinEvent: {}, Window: {})", + winevent, window + ) + } + WindowManagerEvent::Hide(winevent, window) => { + write!(f, "Hide (WinEvent: {}, Window: {})", winevent, window) + } + WindowManagerEvent::Minimize(winevent, window) => { + write!(f, "Minimize (WinEvent: {}, Window: {})", winevent, window) + } + WindowManagerEvent::Show(winevent, window) => { + write!(f, "Show (WinEvent: {}, Window: {})", winevent, window) + } + WindowManagerEvent::MoveResizeStart(winevent, window) => { + write!( + f, + "MoveResizeStart (WinEvent: {}, Window: {})", + winevent, window + ) + } + WindowManagerEvent::MoveResizeEnd(winevent, window) => { + write!( + f, + "MoveResizeEnd (WinEvent: {}, Window: {})", + winevent, window + ) + } + WindowManagerEvent::MouseCapture(winevent, window) => { + write!( + f, + "MouseCapture (WinEvent: {}, Window: {})", + winevent, window + ) + } + } + } +} + +impl WindowManagerEvent { + pub const fn from_win_event(winevent: WinEvent, window: Window) -> Option { + match winevent { + WinEvent::ObjectDestroy => Some(Self::Destroy(winevent, window)), + + WinEvent::ObjectCloaked | WinEvent::ObjectHide => Some(Self::Hide(winevent, window)), + + WinEvent::SystemMinimizeStart => Some(Self::Minimize(winevent, window)), + + WinEvent::ObjectShow | WinEvent::ObjectUncloaked | WinEvent::SystemMinimizeEnd => { + Some(Self::Show(winevent, window)) + } + + WinEvent::ObjectFocus | WinEvent::SystemForeground => { + Some(Self::FocusChange(winevent, window)) + } + WinEvent::SystemMoveSizeStart => Some(Self::MoveResizeStart(winevent, window)), + WinEvent::SystemMoveSizeEnd => Some(Self::MoveResizeEnd(winevent, window)), + WinEvent::SystemCaptureStart | WinEvent::SystemCaptureEnd => { + Some(Self::MouseCapture(winevent, window)) + } + _ => None, + } + } +} diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs new file mode 100644 index 00000000..a7cedc36 --- /dev/null +++ b/komorebi/src/windows_api.rs @@ -0,0 +1,470 @@ +use std::collections::VecDeque; +use std::convert::TryFrom; +use std::convert::TryInto; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; +use eyre::Error; + +use bindings::Windows::Win32::Foundation::BOOL; +use bindings::Windows::Win32::Foundation::HANDLE; +use bindings::Windows::Win32::Foundation::HWND; +use bindings::Windows::Win32::Foundation::LPARAM; +use bindings::Windows::Win32::Foundation::POINT; +use bindings::Windows::Win32::Foundation::PWSTR; +use bindings::Windows::Win32::Graphics::Dwm::DwmGetWindowAttribute; +use bindings::Windows::Win32::Graphics::Dwm::DWMWA_CLOAKED; +use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP; +use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED; +use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL; +use bindings::Windows::Win32::Graphics::Gdi::EnumDisplayMonitors; +use bindings::Windows::Win32::Graphics::Gdi::MonitorFromWindow; +use bindings::Windows::Win32::Graphics::Gdi::HDC; +use bindings::Windows::Win32::Graphics::Gdi::MONITORENUMPROC; +use bindings::Windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST; +use bindings::Windows::Win32::Graphics::Gdi::{GetMonitorInfoW, HMONITOR, MONITORINFO}; +use bindings::Windows::Win32::System::Threading::AttachThreadInput; +use bindings::Windows::Win32::System::Threading::GetCurrentProcessId; +use bindings::Windows::Win32::System::Threading::GetCurrentThreadId; +use bindings::Windows::Win32::System::Threading::OpenProcess; +use bindings::Windows::Win32::System::Threading::QueryFullProcessImageNameW; +use bindings::Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS; +use bindings::Windows::Win32::System::Threading::PROCESS_NAME_FORMAT; +use bindings::Windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION; +use bindings::Windows::Win32::UI::KeyboardAndMouseInput::SetFocus; +use bindings::Windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EnumWindows; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GetCursorPos; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowRect; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowTextW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; +use bindings::Windows::Win32::UI::WindowsAndMessaging::IsIconic; +use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindow; +use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindowVisible; +use bindings::Windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SetCursorPos; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowPos; +use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_STYLE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_HIDE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC; +use komorebi_core::Rect; + +use crate::container::Container; +use crate::monitor::Monitor; +use crate::ring::Ring; +use crate::workspace::Workspace; +use crate::{monitor, windows_callbacks}; + +pub enum WindowsResult { + Err(E), + Ok(T), +} + +impl From for WindowsResult<(), Error> { + fn from(return_value: BOOL) -> Self { + if return_value.as_bool() { + Self::Ok(()) + } else { + Self::Err(std::io::Error::last_os_error().into()) + } + } +} + +impl From for WindowsResult { + fn from(return_value: HWND) -> Self { + if return_value.is_null() { + Self::Err(std::io::Error::last_os_error().into()) + } else { + Self::Ok(return_value.0) + } + } +} + +impl From for WindowsResult { + fn from(return_value: HANDLE) -> Self { + if return_value.is_null() { + Self::Err(std::io::Error::last_os_error().into()) + } else { + Self::Ok(return_value) + } + } +} + +impl From for WindowsResult { + fn from(return_value: isize) -> Self { + match return_value { + 0 => Self::Err(std::io::Error::last_os_error().into()), + _ => Self::Ok(return_value), + } + } +} + +impl From for WindowsResult { + fn from(return_value: u32) -> Self { + match return_value { + 0 => Self::Err(std::io::Error::last_os_error().into()), + _ => Self::Ok(return_value), + } + } +} + +impl From for WindowsResult { + fn from(return_value: i32) -> Self { + match return_value { + 0 => Self::Err(std::io::Error::last_os_error().into()), + _ => Self::Ok(return_value), + } + } +} + +impl From> for Result { + fn from(result: WindowsResult) -> Self { + match result { + WindowsResult::Err(error) => Self::Err(error), + WindowsResult::Ok(ok) => Self::Ok(ok), + } + } +} + +pub struct WindowsApi; + +impl WindowsApi { + pub fn enum_display_monitors( + callback: MONITORENUMPROC, + callback_data_address: isize, + ) -> Result<()> { + Result::from(WindowsResult::from(unsafe { + EnumDisplayMonitors( + HDC(0), + std::ptr::null_mut(), + Option::from(callback), + LPARAM(callback_data_address), + ) + })) + } + + pub fn load_monitor_information(monitors: &mut Ring) -> Result<()> { + Self::enum_display_monitors( + windows_callbacks::enum_display_monitor, + monitors as *mut Ring as isize, + ) + } + + pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> { + Result::from(WindowsResult::from(unsafe { + EnumWindows(Option::from(callback), LPARAM(callback_data_address)) + })) + } + pub fn load_workspace_information(monitors: &mut Ring) -> Result<()> { + for monitor in monitors.elements_mut() { + if monitor.workspaces().is_empty() { + let mut workspace = Workspace::default(); + + // EnumWindows will enumerate through windows on all monitors + Self::enum_windows( + windows_callbacks::enum_window, + workspace.containers_mut() as *mut VecDeque as isize, + )?; + + // So we have to prune each monitor's primary workspace of undesired windows here + let mut windows_on_other_monitors = vec![]; + + for container in workspace.containers_mut() { + for window in container.windows() { + if Self::monitor_from_window(window.hwnd()) != monitor.id() { + windows_on_other_monitors.push(window.hwnd().0); + } + } + } + + for hwnd in windows_on_other_monitors { + workspace.remove_window(hwnd)?; + } + + monitor.workspaces_mut().push_back(workspace); + } + } + + Ok(()) + } + + pub fn allow_set_foreground_window(process_id: u32) -> Result<()> { + Result::from(WindowsResult::from(unsafe { + AllowSetForegroundWindow(process_id) + })) + } + + pub fn monitor_from_window(hwnd: HWND) -> isize { + // MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow + unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0 + } + + pub fn set_window_pos(hwnd: HWND, layout: &Rect) -> Result<()> { + Result::from(WindowsResult::from(unsafe { + SetWindowPos( + hwnd, + HWND_NOTOPMOST, + layout.left, + layout.top, + layout.right, + layout.bottom, + SWP_NOACTIVATE, + ) + })) + } + + fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) { + // BOOL is returned but does not signify whether or not the operation was succesful + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow + unsafe { ShowWindow(hwnd, command) }; + } + + pub fn hide_window(hwnd: HWND) { + Self::show_window(hwnd, SW_HIDE); + } + + pub fn restore_window(hwnd: HWND) { + Self::show_window(hwnd, SW_RESTORE); + } + + pub fn set_foreground_window(hwnd: HWND) -> Result<()> { + match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) { + WindowsResult::Ok(_) => Ok(()), + WindowsResult::Err(error) => { + // TODO: Figure out the odd behaviour here, docs state that a zero value means + // TODO: that the window was not brought to the foreground, but this contradicts + // TODO: the behaviour that I have observed which resulted in this check + if error.to_string() == "The operation completed successfully. (os error 0)" { + Ok(()) + } else { + Err(error) + } + } + } + } + + pub fn foreground_window() -> Result { + Result::from(WindowsResult::from(unsafe { GetForegroundWindow().0 })) + } + + pub fn window_rect(hwnd: HWND) -> Result { + let mut rect = unsafe { std::mem::zeroed() }; + + Result::from(WindowsResult::from(unsafe { + GetWindowRect(hwnd, &mut rect) + }))?; + + Ok(Rect::from(rect)) + } + + fn set_cursor_pos(x: i32, y: i32) -> Result<()> { + Result::from(WindowsResult::from(unsafe { SetCursorPos(x, y) })) + } + + pub fn cursor_pos() -> Result { + let mut cursor_pos: POINT = unsafe { std::mem::zeroed() }; + + Result::from(WindowsResult::from(unsafe { + GetCursorPos(&mut cursor_pos) + }))?; + + Ok(cursor_pos) + } + + pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> { + Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2)) + } + + pub fn window_thread_process_id(hwnd: HWND) -> (u32, u32) { + let mut process_id: u32 = 0; + + // Behaviour is undefined if an invalid HWND is given + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid + let thread_id = unsafe { GetWindowThreadProcessId(hwnd, &mut process_id) }; + + (process_id, thread_id) + } + + pub fn current_thread_id() -> u32 { + unsafe { GetCurrentThreadId() } + } + + pub fn current_process_id() -> u32 { + unsafe { GetCurrentProcessId() } + } + + pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> { + Result::from(WindowsResult::from(unsafe { + AttachThreadInput(thread_id, target_thread_id, attach) + })) + } + + pub fn set_focus(hwnd: HWND) -> Result<()> { + match WindowsResult::from(unsafe { SetFocus(hwnd) }) { + WindowsResult::Ok(_) => Ok(()), + WindowsResult::Err(error) => { + // If the window is not attached to the calling thread's message queue, the return value is NULL + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setfocus + if error.to_string() == "The operation completed successfully. (os error 0)" { + Ok(()) + } else { + Err(error) + } + } + } + } + + fn set_window_long_ptr_w( + hwnd: HWND, + index: WINDOW_LONG_PTR_INDEX, + new_value: isize, + ) -> Result<()> { + Result::from(WindowsResult::from(unsafe { + SetWindowLongPtrW(hwnd, index, new_value) + })) + .map(|_| {}) + } + + pub fn gwl_style(hwnd: HWND) -> Result { + Self::window_long_ptr_w(hwnd, GWL_STYLE) + } + pub fn gwl_ex_style(hwnd: HWND) -> Result { + Self::window_long_ptr_w(hwnd, GWL_EXSTYLE) + } + + fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result { + Result::from(WindowsResult::from(unsafe { + GetWindowLongPtrW(hwnd, index) + })) + } + + pub fn update_style(hwnd: HWND, new_value: isize) -> Result<()> { + Self::set_window_long_ptr_w(hwnd, GWL_STYLE, new_value) + } + + pub fn window_text_w(hwnd: HWND) -> Result { + let mut text: [u16; 512] = [0; 512]; + match WindowsResult::from(unsafe { + GetWindowTextW(hwnd, PWSTR(text.as_mut_ptr()), text.len().try_into()?) + }) { + WindowsResult::Ok(len) => { + let length = usize::try_from(len)?; + Ok(String::from_utf16(&text[..length])?) + } + WindowsResult::Err(error) => Err(error), + } + } + + fn open_process( + access_rights: PROCESS_ACCESS_RIGHTS, + inherit_handle: bool, + process_id: u32, + ) -> Result { + Result::from(WindowsResult::from(unsafe { + OpenProcess(access_rights, inherit_handle, process_id) + })) + } + + pub fn process_handle(process_id: u32) -> Result { + Self::open_process(PROCESS_QUERY_INFORMATION, false, process_id) + } + + pub fn exe_path(handle: HANDLE) -> Result { + let mut len = 260_u32; + let mut path: Vec = vec![0; len as usize]; + let text_ptr = path.as_mut_ptr(); + + Result::from(WindowsResult::from(unsafe { + QueryFullProcessImageNameW( + handle, + PROCESS_NAME_FORMAT(0), + PWSTR(text_ptr), + &mut len as *mut u32, + ) + }))?; + + Ok(String::from_utf16(&path[..len as usize])?) + } + + pub fn exe(handle: HANDLE) -> Result { + Ok(Self::exe_path(handle)? + .split('\\') + .last() + .context("there is no last element")? + .to_string()) + } + + pub fn real_window_class_w(hwnd: HWND) -> Result { + const BUF_SIZE: usize = 512; + let mut class: [u16; BUF_SIZE] = [0; BUF_SIZE]; + + let len = Result::from(WindowsResult::from(unsafe { + RealGetWindowClassW(hwnd, PWSTR(class.as_mut_ptr()), u32::try_from(BUF_SIZE)?) + }))?; + + Ok(String::from_utf16(&class[0..len as usize])?) + } + + pub fn is_window_cloaked(hwnd: HWND) -> Result { + let mut cloaked: u32 = 0; + + unsafe { + DwmGetWindowAttribute( + hwnd, + std::mem::transmute::<_, u32>(DWMWA_CLOAKED), + (&mut cloaked as *mut u32).cast(), + u32::try_from(std::mem::size_of::())?, + )?; + } + + Ok(matches!( + cloaked, + DWM_CLOAKED_APP | DWM_CLOAKED_SHELL | DWM_CLOAKED_INHERITED + )) + } + + pub fn is_window(hwnd: HWND) -> bool { + unsafe { IsWindow(hwnd) }.into() + } + + pub fn is_window_visible(hwnd: HWND) -> bool { + unsafe { IsWindowVisible(hwnd) }.into() + } + + pub fn is_iconic(hwnd: HWND) -> bool { + unsafe { IsIconic(hwnd) }.into() + } + + pub fn monitor_info_w(hmonitor: HMONITOR) -> Result { + let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() }; + monitor_info.cbSize = u32::try_from(std::mem::size_of::())?; + + Result::from(WindowsResult::from(unsafe { + GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast()) + }))?; + + Ok(monitor_info) + } + + pub fn monitor(hmonitor: HMONITOR) -> Result { + let monitor_info = Self::monitor_info_w(hmonitor)?; + + Ok(monitor::new( + hmonitor.0, + monitor_info.rcMonitor.into(), + monitor_info.rcWork.into(), + )) + } +} diff --git a/komorebi/src/windows_callbacks.rs b/komorebi/src/windows_callbacks.rs new file mode 100644 index 00000000..9f4d0041 --- /dev/null +++ b/komorebi/src/windows_callbacks.rs @@ -0,0 +1,125 @@ +use std::collections::VecDeque; + +use bindings::Windows::Win32::Foundation::BOOL; +use bindings::Windows::Win32::Foundation::HWND; +use bindings::Windows::Win32::Foundation::LPARAM; +use bindings::Windows::Win32::Foundation::RECT; +use bindings::Windows::Win32::Graphics::Gdi::HDC; +use bindings::Windows::Win32::Graphics::Gdi::HMONITOR; +use bindings::Windows::Win32::UI::Accessibility::HWINEVENTHOOK; + +use crate::container::Container; +use crate::monitor::Monitor; +use crate::ring::Ring; +use crate::styles::GwlStyle; +use crate::window::Window; +use crate::window_manager_event::WindowManagerEvent; +use crate::windows_api::WindowsApi; +use crate::winevent::WinEvent; +use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL; + +pub extern "system" fn enum_display_monitor( + hmonitor: HMONITOR, + _: HDC, + _: *mut RECT, + lparam: LPARAM, +) -> BOOL { + let monitors = unsafe { &mut *(lparam.0 as *mut Ring) }; + if let Ok(m) = WindowsApi::monitor(hmonitor) { + monitors.elements_mut().push_back(m); + } + + true.into() +} + +pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL { + let containers = unsafe { &mut *(lparam.0 as *mut VecDeque) }; + + let is_visible = WindowsApi::is_window_visible(hwnd); + let is_window = WindowsApi::is_window(hwnd); + let is_minimized = WindowsApi::is_iconic(hwnd); + + if is_visible && is_window && !is_minimized { + let mut window = Window { + hwnd: hwnd.0, + original_style: GwlStyle::empty(), + }; + + if let Ok(style) = window.style() { + window.original_style = style; + } + + if let Ok(should_manage) = window.should_manage(None) { + if should_manage { + let mut container = Container::default(); + container.windows_mut().push_back(window); + containers.push_back(container); + } + } + } + + true.into() +} + +pub extern "system" fn win_event_hook( + _h_win_event_hook: HWINEVENTHOOK, + event: u32, + hwnd: HWND, + id_object: i32, + _id_child: i32, + _id_event_thread: u32, + _dwms_event_time: u32, +) { + // OBJID_WINDOW + if id_object != 0 { + return; + } + + let mut window = Window { + hwnd: hwnd.0, + original_style: GwlStyle::empty(), + }; + + if let Ok(style) = window.style() { + window.original_style = style; + } + + let winevent = unsafe { ::std::mem::transmute(event) }; + let event_type = if let Some(event) = WindowManagerEvent::from_win_event(winevent, window) { + event + } else { + // Some apps like Firefox don't send ObjectCreate or ObjectShow on launch + // This spams the message queue, but I don't know what else to do. On launch + // it only sends the following WinEvents :/ + // + // [yatta\src\windows_event.rs:110] event = 32780 ObjectNameChange + // [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange + let object_name_change_on_launch = + vec!["firefox.exe".to_string(), "idea64.exe".to_string()]; + + if let Ok(exe) = window.exe() { + if winevent == WinEvent::ObjectNameChange { + if object_name_change_on_launch.contains(&exe) { + WindowManagerEvent::Show(winevent, window) + } else { + return; + } + } else { + return; + } + } else { + return; + } + }; + + if let Ok(should_manage) = window.should_manage(Option::from(event_type)) { + if should_manage { + WINEVENT_CALLBACK_CHANNEL + .lock() + .unwrap() + .0 + .send(event_type) + .expect("could not send message on WINEVENT_CALLBACK_CHANNEL"); + } + } +} diff --git a/komorebi/src/winevent.rs b/komorebi/src/winevent.rs new file mode 100644 index 00000000..e9209ae6 --- /dev/null +++ b/komorebi/src/winevent.rs @@ -0,0 +1,173 @@ +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_START; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_CARET; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END_APPLICATION; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_LAYOUT; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_START_APPLICATION; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_REGION; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SCROLL; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SIMPLE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_ACCELERATORCHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CLOAKED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CONTENTSCROLLED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CREATE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DEFACTIONCHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESCRIPTIONCHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCANCEL; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCOMPLETE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGDROPPED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGENTER; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGLEAVE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_END; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_FOCUS; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HELPCHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HIDE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_CHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_HIDE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_SHOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_INVOKED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LIVEREGIONCHANGED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_NAMECHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_PARENTCHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_REORDER; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTION; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONADD; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONREMOVE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONWITHIN; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SHOW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_STATECHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTSELECTIONCHANGED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_UNCLOAKED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_VALUECHANGE; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_END; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_START; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ALERT; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ARRANGMENTPREVIEW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTUREEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTURESTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DESKTOPSWITCH; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_END; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_FOREGROUND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_IME_KEY_NOTIFICATION; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZEEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZESTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZEEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZESTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SOUND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHEND; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPDROPPED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPGRABBED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPOVERTARGET; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_CANCELLED; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHSTART; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_END; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START; + +#[derive(Clone, Copy, PartialEq, Debug, strum::Display)] +#[repr(u32)] +pub enum WinEvent { + AiaEnd = EVENT_AIA_END, + AiaStart = EVENT_AIA_START, + ConsoleCaret = EVENT_CONSOLE_CARET, + ConsoleEnd = EVENT_CONSOLE_END, + ConsoleEndApplication = EVENT_CONSOLE_END_APPLICATION, + ConsoleLayout = EVENT_CONSOLE_LAYOUT, + ConsoleStartApplication = EVENT_CONSOLE_START_APPLICATION, + ConsoleUpdateRegion = EVENT_CONSOLE_UPDATE_REGION, + ConsoleUpdateScroll = EVENT_CONSOLE_UPDATE_SCROLL, + ConsoleUpdateSimple = EVENT_CONSOLE_UPDATE_SIMPLE, + ObjectAcceleratorChange = EVENT_OBJECT_ACCELERATORCHANGE, + ObjectCloaked = EVENT_OBJECT_CLOAKED, + ObjectContentScrolled = EVENT_OBJECT_CONTENTSCROLLED, + ObjectCreate = EVENT_OBJECT_CREATE, + ObjectDefActionChange = EVENT_OBJECT_DEFACTIONCHANGE, + ObjectDescriptionChange = EVENT_OBJECT_DESCRIPTIONCHANGE, + ObjectDestroy = EVENT_OBJECT_DESTROY, + ObjectDragCancel = EVENT_OBJECT_DRAGCANCEL, + ObjectDragComplete = EVENT_OBJECT_DRAGCOMPLETE, + ObjectDragDropped = EVENT_OBJECT_DRAGDROPPED, + ObjectDragEnter = EVENT_OBJECT_DRAGENTER, + ObjectDragLeave = EVENT_OBJECT_DRAGLEAVE, + ObjectDragStart = EVENT_OBJECT_DRAGSTART, + ObjectEnd = EVENT_OBJECT_END, + ObjectFocus = EVENT_OBJECT_FOCUS, + ObjectHelpChange = EVENT_OBJECT_HELPCHANGE, + ObjectHide = EVENT_OBJECT_HIDE, + ObjectHostedObjectsInvalidated = EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED, + ObjectImeChange = EVENT_OBJECT_IME_CHANGE, + ObjectImeHide = EVENT_OBJECT_IME_HIDE, + ObjectImeShow = EVENT_OBJECT_IME_SHOW, + ObjectInvoked = EVENT_OBJECT_INVOKED, + ObjectLiveRegionChanged = EVENT_OBJECT_LIVEREGIONCHANGED, + ObjectLocationChange = EVENT_OBJECT_LOCATIONCHANGE, + ObjectNameChange = EVENT_OBJECT_NAMECHANGE, + ObjectParentChange = EVENT_OBJECT_PARENTCHANGE, + ObjectReorder = EVENT_OBJECT_REORDER, + ObjectSelection = EVENT_OBJECT_SELECTION, + ObjectSelectionAdd = EVENT_OBJECT_SELECTIONADD, + ObjectSelectionRemove = EVENT_OBJECT_SELECTIONREMOVE, + ObjectSelectionWithin = EVENT_OBJECT_SELECTIONWITHIN, + ObjectShow = EVENT_OBJECT_SHOW, + ObjectStateChange = EVENT_OBJECT_STATECHANGE, + ObjectTextEditConversionTargetChanged = EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED, + ObjectTextSelectionChanged = EVENT_OBJECT_TEXTSELECTIONCHANGED, + ObjectUncloaked = EVENT_OBJECT_UNCLOAKED, + ObjectValueChange = EVENT_OBJECT_VALUECHANGE, + OemDefinedEnd = EVENT_OEM_DEFINED_END, + OemDefinedStart = EVENT_OEM_DEFINED_START, + SystemAlert = EVENT_SYSTEM_ALERT, + SystemArrangementPreview = EVENT_SYSTEM_ARRANGMENTPREVIEW, + SystemCaptureEnd = EVENT_SYSTEM_CAPTUREEND, + SystemCaptureStart = EVENT_SYSTEM_CAPTURESTART, + SystemContextHelpEnd = EVENT_SYSTEM_CONTEXTHELPEND, + SystemContextHelpStart = EVENT_SYSTEM_CONTEXTHELPSTART, + SystemDesktopSwitch = EVENT_SYSTEM_DESKTOPSWITCH, + SystemDialogEnd = EVENT_SYSTEM_DIALOGEND, + SystemDialogStart = EVENT_SYSTEM_DIALOGSTART, + SystemDragDropEnd = EVENT_SYSTEM_DRAGDROPEND, + SystemDragDropStart = EVENT_SYSTEM_DRAGDROPSTART, + SystemEnd = EVENT_SYSTEM_END, + SystemForeground = EVENT_SYSTEM_FOREGROUND, + SystemImeKeyNotification = EVENT_SYSTEM_IME_KEY_NOTIFICATION, + SystemMenuEnd = EVENT_SYSTEM_MENUEND, + SystemMenuPopupEnd = EVENT_SYSTEM_MENUPOPUPEND, + SystemMenuPopupStart = EVENT_SYSTEM_MENUPOPUPSTART, + SystemMenuStart = EVENT_SYSTEM_MENUSTART, + SystemMinimizeEnd = EVENT_SYSTEM_MINIMIZEEND, + SystemMinimizeStart = EVENT_SYSTEM_MINIMIZESTART, + SystemMoveSizeEnd = EVENT_SYSTEM_MOVESIZEEND, + SystemMoveSizeStart = EVENT_SYSTEM_MOVESIZESTART, + SystemScrollingEnd = EVENT_SYSTEM_SCROLLINGEND, + SystemScrollingStart = EVENT_SYSTEM_SCROLLINGSTART, + SystemSound = EVENT_SYSTEM_SOUND, + SystemSwitchEnd = EVENT_SYSTEM_SWITCHEND, + SystemSwitchStart = EVENT_SYSTEM_SWITCHSTART, + SystemSwitcherAppDropped = EVENT_SYSTEM_SWITCHER_APPDROPPED, + SystemSwitcherAppGrabbed = EVENT_SYSTEM_SWITCHER_APPGRABBED, + SystemSwitcherAppOverTarget = EVENT_SYSTEM_SWITCHER_APPOVERTARGET, + SystemSwitcherCancelled = EVENT_SYSTEM_SWITCHER_CANCELLED, + UiaEventIdSEnd = EVENT_UIA_EVENTID_END, + UiaEventIdStart = EVENT_UIA_EVENTID_START, + UiaPropIdSEnd = EVENT_UIA_PROPID_END, + UiaPropIdStart = EVENT_UIA_PROPID_START, +} diff --git a/komorebi/src/winevent_listener.rs b/komorebi/src/winevent_listener.rs new file mode 100644 index 00000000..7d979a91 --- /dev/null +++ b/komorebi/src/winevent_listener.rs @@ -0,0 +1,107 @@ +use std::sync::atomic::AtomicIsize; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; +use std::time::Duration; + +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use lazy_static::lazy_static; + +use bindings::Windows::Win32::Foundation::HWND; +use bindings::Windows::Win32::UI::Accessibility::SetWinEventHook; +use bindings::Windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::PeekMessageW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::TranslateMessage; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MAX; +use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MIN; +use bindings::Windows::Win32::UI::WindowsAndMessaging::MSG; +use bindings::Windows::Win32::UI::WindowsAndMessaging::PM_REMOVE; + +use crate::window_manager_event::WindowManagerEvent; +use crate::windows_callbacks; + +lazy_static! { + pub static ref WINEVENT_CALLBACK_CHANNEL: Arc, Receiver)>> = + Arc::new(Mutex::new(crossbeam_channel::unbounded())); +} + +#[derive(Debug, Clone)] +pub struct WinEventListener { + hook: Arc, + outgoing_events: Arc>>, +} + +pub fn new(outgoing: Arc>>) -> WinEventListener { + WinEventListener { + hook: Arc::new(AtomicIsize::new(0)), + outgoing_events: outgoing, + } +} + +impl WinEventListener { + pub fn start(self) { + let hook = self.hook.clone(); + let outgoing = self.outgoing_events.lock().unwrap().clone(); + + thread::spawn(move || unsafe { + let hook_ref = SetWinEventHook( + EVENT_MIN as u32, + EVENT_MAX as u32, + None, + Some(windows_callbacks::win_event_hook), + 0, + 0, + 0, + ); + + hook.store(hook_ref.0, Ordering::SeqCst); + + // The code in the callback doesn't work in its own loop, needs to be within + // the MessageLoop callback for the winevent callback to even fire + MessageLoop::start(10, |_msg| { + if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().unwrap().1.try_recv() { + match outgoing.send(event) { + Ok(_) => {} + Err(error) => { + tracing::error!("{}", error); + } + } + } + + true + }); + }); + } +} + +#[derive(Debug, Copy, Clone)] +pub struct MessageLoop; + +impl MessageLoop { + pub fn start(sleep: u64, cb: impl Fn(Option) -> bool) { + Self::start_with_sleep(sleep, cb); + } + + fn start_with_sleep(sleep: u64, cb: impl Fn(Option) -> bool) { + let mut msg: MSG = MSG::default(); + loop { + let mut value: Option = None; + unsafe { + if !bool::from(!PeekMessageW(&mut msg, HWND(0), 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + + value = Some(msg); + } + } + + thread::sleep(Duration::from_millis(sleep)); + + if !cb(value) { + break; + } + } + } +} diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs new file mode 100644 index 00000000..93f3e712 --- /dev/null +++ b/komorebi/src/workspace.rs @@ -0,0 +1,537 @@ +use std::collections::VecDeque; + +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; + +use komorebi_core::Layout; +use komorebi_core::LayoutFlip; +use komorebi_core::OperationDirection; +use komorebi_core::Rect; + +use crate::container::Container; +use crate::ring::Ring; +use crate::window::Window; +use crate::windows_api::WindowsApi; + +#[derive(Debug, Clone)] +pub struct Workspace { + name: Option, + containers: Ring, + monocle_container: Option, + monocle_restore_idx: Option, + floating_windows: Vec, + layout: Layout, + layout_flip: Option, + workspace_padding: Option, + container_padding: Option, + latest_layout: Vec, +} + +impl Default for Workspace { + fn default() -> Self { + Self { + name: None, + containers: Ring::default(), + monocle_container: None, + monocle_restore_idx: None, + floating_windows: Vec::default(), + layout: Layout::BSP, + layout_flip: None, + workspace_padding: Option::from(20), + container_padding: Option::from(5), + latest_layout: vec![], + } + } +} + +impl Workspace { + pub fn hide(&mut self) { + for container in self.containers_mut() { + for window in container.windows_mut() { + window.hide(); + } + } + } + + pub fn restore(&mut self) { + for container in self.containers_mut() { + if let Some(window) = container.visible_window_mut() { + window.restore(); + } + } + } + + pub fn update(&mut self, work_area: &Rect) -> Result<()> { + let mut adjusted_work_area = work_area.clone(); + adjusted_work_area.add_padding(self.workspace_padding()); + + if let Some(container) = self.monocle_container_mut() { + if let Some(window) = container.focused_window_mut() { + window.set_position(&adjusted_work_area)?; + window.focus()?; + } + } else { + let layouts = self.layout().calculate( + &adjusted_work_area, + self.containers().len(), + self.container_padding(), + self.layout_flip(), + ); + + let windows = self.visible_windows_mut(); + for (i, window) in windows.into_iter().enumerate() { + if let (Some(window), Some(layout)) = (window, layouts.get(i)) { + window.set_position(layout)?; + } + } + + self.set_latest_layout(layouts); + } + + Ok(()) + } + + pub fn reap_orphans(&mut self) -> Result<(usize, usize)> { + let mut hwnds = vec![]; + for window in self.visible_windows_mut().into_iter().flatten() { + if !window.is_window() { + hwnds.push(window.hwnd); + } + } + + for hwnd in &hwnds { + tracing::debug!("reaping hwnd: {}", hwnd); + self.remove_window(*hwnd)?; + } + + let mut container_ids = vec![]; + for container in self.containers() { + if container.windows().is_empty() { + container_ids.push(container.id().clone()); + } + } + + self.containers_mut() + .retain(|c| !container_ids.contains(c.id())); + + Ok((hwnds.len(), container_ids.len())) + } + + pub fn focus_container_by_window(&mut self, hwnd: isize) -> Result<()> { + let container_idx = self + .container_idx_for_window(hwnd) + .context("there is no container/window")?; + + let container = self + .containers_mut() + .get_mut(container_idx) + .context("there is no container")?; + + let window_idx = container + .idx_for_window(hwnd) + .context("there is no window")?; + + container.focus_window(window_idx); + self.focus_container(container_idx); + + Ok(()) + } + + pub fn container_idx_from_current_point(&self) -> Option { + let mut idx = None; + + let point = WindowsApi::cursor_pos().ok()?; + + for (i, _container) in self.containers().iter().enumerate() { + if let Some(rect) = self.latest_layout().get(i) { + if rect.contains_point((point.x, point.y)) { + idx = Option::from(i); + } + } + } + + idx + } + + pub fn contains_window(&self, hwnd: isize) -> bool { + for x in self.containers() { + if x.contains_window(hwnd) { + return true; + } + } + + false + } + + pub fn promote_container(&mut self) -> Result<()> { + let container = self + .remove_focused_container() + .context("there is no container")?; + self.containers_mut().push_front(container); + self.focus_container(0); + + Ok(()) + } + + pub fn add_container(&mut self, container: Container) { + self.containers_mut().push_back(container); + self.focus_container(self.containers().len() - 1); + } + + fn remove_container_by_idx(&mut self, idx: usize) -> Option { + self.containers_mut().remove(idx) + } + + fn container_idx_for_window(&mut self, hwnd: isize) -> Option { + let mut idx = None; + for (i, x) in self.containers().iter().enumerate() { + if x.contains_window(hwnd) { + idx = Option::from(i); + } + } + + idx + } + + pub fn remove_window(&mut self, hwnd: isize) -> Result<()> { + let container_idx = self + .container_idx_for_window(hwnd) + .context("there is no window")?; + + let container = self + .containers_mut() + .get_mut(container_idx) + .context("there is no container")?; + + let window_idx = container + .windows() + .iter() + .position(|window| window.hwnd == hwnd) + .context("there is no window")?; + + container + .remove_window_by_idx(window_idx) + .context("there is no window")?; + + if container.windows().is_empty() { + self.containers_mut() + .remove(container_idx) + .context("there is no container")?; + } + + if container_idx != 0 { + self.focus_container(container_idx - 1); + } + + Ok(()) + } + + pub fn remove_focused_container(&mut self) -> Option { + let focused_idx = self.focused_container_idx(); + let container = self.remove_container_by_idx(focused_idx); + + if focused_idx != 0 { + self.focus_container(focused_idx - 1); + } + + container + } + + pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option { + if direction.is_valid( + self.layout, + self.focused_container_idx(), + self.containers().len(), + ) { + Option::from(direction.new_idx(self.layout, self.containers.focused_idx())) + } else { + None + } + } + + pub fn move_window_to_container(&mut self, target_container_idx: usize) -> Result<()> { + let focused_idx = self.focused_container_idx(); + + let container = self + .focused_container_mut() + .context("there is no container")?; + + let window = container + .remove_focused_window() + .context("there is no window")?; + + // This is a little messy + let adjusted_target_container_index = if container.windows().is_empty() { + self.containers_mut().remove(focused_idx); + if focused_idx < target_container_idx { + target_container_idx - 1 + } else { + target_container_idx + } + } else { + container.load_focused_window(); + target_container_idx + }; + + let target_container = self + .containers_mut() + .get_mut(adjusted_target_container_index) + .context("there is no container")?; + + target_container.add_window(window); + + self.focus_container(adjusted_target_container_index); + self.focused_container_mut() + .context("there is no container")? + .load_focused_window(); + + Ok(()) + } + + pub fn new_container_for_focused_window(&mut self) -> Result<()> { + let focused_container_idx = self.focused_container_idx(); + + let container = self + .focused_container_mut() + .context("there is no container")?; + + let window = container + .remove_focused_window() + .context("there is no window")?; + + if container.windows().is_empty() { + self.containers_mut().remove(focused_container_idx); + } else { + container.load_focused_window(); + } + + self.new_container_for_window(window); + + let mut container = Container::default(); + container.add_window(window); + Ok(()) + } + + pub fn new_container_for_floating_window(&mut self) -> Result<()> { + let focused_idx = self.focused_container_idx(); + let window = self + .remove_focused_floating_window() + .context("there is no floating window")?; + + let mut container = Container::default(); + container.add_window(window); + self.containers_mut().insert(focused_idx, container); + + Ok(()) + } + + pub fn new_container_for_window(&mut self, window: Window) { + let focused_idx = self.focused_container_idx(); + let len = self.containers().len(); + + let mut container = Container::default(); + container.add_window(window); + + if focused_idx == len - 1 { + self.containers_mut().resize(len, Container::default()); + } + + self.containers_mut().insert(focused_idx + 1, container); + self.focus_container(focused_idx + 1); + } + + pub fn new_floating_window(&mut self) -> Result<()> { + let focused_idx = self.focused_container_idx(); + + let container = self + .focused_container_mut() + .context("there is no container")?; + + let window = container + .remove_focused_window() + .context("there is no window")?; + + if container.windows().is_empty() { + self.containers_mut().remove(focused_idx); + } else { + container.load_focused_window(); + } + + self.floating_windows_mut().push(window); + + Ok(()) + } + + pub fn new_monocle_container(&mut self) -> Result<()> { + let focused_idx = self.focused_container_idx(); + let container = self + .containers_mut() + .remove(focused_idx) + .context("there is not container")?; + + self.monocle_container = Option::from(container); + self.monocle_restore_idx = Option::from(focused_idx); + + if focused_idx != 0 { + self.focus_container(focused_idx - 1); + } + + self.monocle_container_mut() + .context("there is no monocle container")? + .load_focused_window(); + + Ok(()) + } + + pub fn reintegrate_monocle_container(&mut self) -> Result<()> { + let restore_idx = self + .monocle_restore_idx() + .context("there is no monocle restore index")?; + + let container = self + .monocle_container_mut() + .context("there is no monocle container")?; + + let container = container.clone(); + if restore_idx > self.containers().len() - 1 { + self.containers_mut() + .resize(restore_idx, Container::default()); + } + + self.containers_mut().insert(restore_idx, container); + self.focus_container(restore_idx); + self.focused_container_mut() + .context("there is no container")? + .load_focused_window(); + + self.monocle_container = None; + + Ok(()) + } + + pub const fn monocle_container(&self) -> Option<&Container> { + self.monocle_container.as_ref() + } + + pub fn monocle_container_mut(&mut self) -> Option<&mut Container> { + self.monocle_container.as_mut() + } + + pub const fn monocle_restore_idx(&self) -> Option { + self.monocle_restore_idx + } + + pub fn focused_container(&self) -> Option<&Container> { + self.containers.focused() + } + + pub const fn focused_container_idx(&self) -> usize { + self.containers.focused_idx() + } + + pub fn focused_container_mut(&mut self) -> Option<&mut Container> { + self.containers.focused_mut() + } + + pub fn focus_container(&mut self, idx: usize) { + tracing::info!("focusing container at index: {}", idx); + self.containers.focus(idx); + } + + pub const fn containers(&self) -> &VecDeque { + self.containers.elements() + } + + pub fn containers_mut(&mut self) -> &mut VecDeque { + self.containers.elements_mut() + } + + pub fn swap_containers(&mut self, i: usize, j: usize) { + tracing::info!("swapping containers: {}, {}", i, j); + self.containers.swap(i, j); + self.focus_container(j); + } + + pub fn remove_focused_floating_window(&mut self) -> Option { + let hwnd = WindowsApi::foreground_window().ok()?; + + let mut idx = None; + for (i, window) in self.floating_windows.iter().enumerate() { + if hwnd == window.hwnd { + idx = Option::from(i); + } + } + + match idx { + None => None, + Some(idx) => { + if self.floating_windows.get(idx).is_some() { + Option::from(self.floating_windows_mut().remove(idx)) + } else { + None + } + } + } + } + + pub const fn floating_windows(&self) -> &Vec { + &self.floating_windows + } + + pub fn floating_windows_mut(&mut self) -> &mut Vec { + self.floating_windows.as_mut() + } + + pub fn visible_windows_mut(&mut self) -> Vec> { + let mut vec = vec![]; + for container in self.containers_mut() { + vec.push(container.visible_window_mut()); + } + + vec + } + + pub const fn layout(&self) -> Layout { + self.layout + } + + pub const fn layout_flip(&self) -> Option { + self.layout_flip + } + + pub const fn workspace_padding(&self) -> Option { + self.workspace_padding + } + + pub const fn container_padding(&self) -> Option { + self.container_padding + } + + pub const fn latest_layout(&self) -> &Vec { + &self.latest_layout + } + + pub fn set_name(&mut self, name: Option) { + self.name = name; + } + + pub fn set_layout(&mut self, layout: Layout) { + self.layout = layout; + } + + pub fn set_layout_flip(&mut self, layout_flip: Option) { + self.layout_flip = layout_flip; + } + + pub fn set_workspace_padding(&mut self, padding: Option) { + self.workspace_padding = padding; + } + + pub fn set_container_padding(&mut self, padding: Option) { + self.container_padding = padding; + } + + pub fn set_latest_layout(&mut self, layout: Vec) { + self.latest_layout = layout; + } +} diff --git a/komorebic/Cargo.toml b/komorebic/Cargo.toml new file mode 100644 index 00000000..f795421c --- /dev/null +++ b/komorebic/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "komorebic" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +komorebi-core = { path = "../komorebi-core" } + +clap = "3.0.0-beta.2" +dirs = "3" +powershell_script = "0.1.5" +uds_windows = "1" +color-eyre = "0.5.11" \ No newline at end of file diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs new file mode 100644 index 00000000..95d73f6a --- /dev/null +++ b/komorebic/src/main.rs @@ -0,0 +1,247 @@ +use std::io::Write; + +use clap::Clap; +use color_eyre::Result; +use uds_windows::UnixStream; + +use komorebi_core::CycleDirection; +use komorebi_core::Layout; +use komorebi_core::LayoutFlip; +use komorebi_core::OperationDirection; +use komorebi_core::Sizing; +use komorebi_core::SocketMessage; + +#[derive(Clap)] +#[clap(version = "1.0", author = "Jade Iqbal ")] +struct Opts { + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Clap)] +enum SubCommand { + Focus(OperationDirection), + Move(OperationDirection), + Stack(OperationDirection), + Unstack, + CycleStack(CycleDirection), + MoveToMonitor(Target), + MoveToWorkspace(Target), + FocusMonitor(Target), + FocusWorkspace(Target), + Promote, + Retile, + ContainerPadding(SizeForMonitorWorkspace), + WorkspacePadding(SizeForMonitorWorkspace), + WorkspaceName(NameForMonitorWorkspace), + ToggleFloat, + TogglePause, + ToggleMonocle, + Start, + Stop, + FloatClass(FloatTarget), + FloatExe(FloatTarget), + FloatTitle(FloatTarget), + AdjustContainerPadding(SizingAdjustment), + AdjustWorkspacePadding(SizingAdjustment), + FlipLayout(LayoutFlip), + Layout(LayoutForMonitorWorkspace), +} + +#[derive(Clap)] +struct SizeForMonitorWorkspace { + monitor: usize, + workspace: usize, + size: i32, +} + +#[derive(Clap)] +struct NameForMonitorWorkspace { + monitor: usize, + workspace: usize, + value: String, +} + +#[derive(Clap)] +struct LayoutForMonitorWorkspace { + monitor: usize, + workspace: usize, + layout: Layout, +} + +#[derive(Clap)] +struct Target { + number: usize, +} + +#[derive(Clap)] +struct SizingAdjustment { + sizing: Sizing, + adjustment: i32, +} + +#[derive(Clap)] +struct FloatTarget { + id: String, +} + +pub fn send_message(bytes: &[u8]) { + let mut socket = dirs::home_dir().unwrap(); + socket.push("komorebi.sock"); + let socket = socket.as_path(); + + let mut stream = match UnixStream::connect(&socket) { + Err(_) => panic!("server is not running"), + Ok(stream) => stream, + }; + + if stream.write_all(&*bytes).is_err() { + panic!("couldn't send message") + } +} + +fn main() -> Result<()> { + let opts: Opts = Opts::parse(); + + match opts.subcmd { + SubCommand::Focus(direction) => { + let bytes = SocketMessage::FocusWindow(direction).as_bytes()?; + send_message(&*bytes); + } + SubCommand::Promote => { + let bytes = SocketMessage::Promote.as_bytes()?; + send_message(&*bytes); + } + SubCommand::TogglePause => { + let bytes = SocketMessage::TogglePause.as_bytes()?; + send_message(&*bytes); + } + SubCommand::Retile => { + let bytes = SocketMessage::Retile.as_bytes()?; + send_message(&*bytes); + } + SubCommand::Move(direction) => { + let bytes = SocketMessage::MoveWindow(direction).as_bytes()?; + send_message(&*bytes); + } + SubCommand::MoveToMonitor(display) => { + let bytes = SocketMessage::MoveContainerToMonitorNumber(display.number) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::MoveToWorkspace(workspace) => { + let bytes = SocketMessage::MoveContainerToWorkspaceNumber(workspace.number) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::ContainerPadding(gap) => { + let bytes = SocketMessage::ContainerPadding(gap.monitor, gap.workspace, gap.size) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::WorkspacePadding(gap) => { + let bytes = SocketMessage::WorkspacePadding(gap.monitor, gap.workspace, gap.size) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::AdjustWorkspacePadding(sizing_adjustment) => { + let bytes = SocketMessage::AdjustWorkspacePadding( + sizing_adjustment.sizing, + sizing_adjustment.adjustment, + ) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::AdjustContainerPadding(sizing_adjustment) => { + let bytes = SocketMessage::AdjustContainerPadding( + sizing_adjustment.sizing, + sizing_adjustment.adjustment, + ) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::ToggleFloat => { + let bytes = SocketMessage::ToggleFloat.as_bytes()?; + send_message(&*bytes); + } + SubCommand::ToggleMonocle => { + let bytes = SocketMessage::ToggleMonocle.as_bytes()?; + send_message(&*bytes); + } + SubCommand::Layout(layout) => { + let bytes = SocketMessage::SetLayout(layout.monitor, layout.workspace, layout.layout) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::Start => { + let script = r#"Start-Process komorebi -WindowStyle hidden"#; + match powershell_script::run(script, true) { + Ok(output) => { + println!("{}", output); + } + Err(e) => { + println!("Error: {}", e); + } + } + } + SubCommand::Stop => { + let bytes = SocketMessage::Stop.as_bytes()?; + send_message(&*bytes); + } + SubCommand::FloatClass(target) => { + let bytes = SocketMessage::FloatClass(target.id).as_bytes()?; + send_message(&*bytes); + } + SubCommand::FloatExe(target) => { + let bytes = SocketMessage::FloatExe(target.id).as_bytes()?; + send_message(&*bytes); + } + SubCommand::FloatTitle(target) => { + let bytes = SocketMessage::FloatTitle(target.id).as_bytes()?; + send_message(&*bytes); + } + SubCommand::Stack(direction) => { + let bytes = SocketMessage::StackWindow(direction).as_bytes()?; + send_message(&*bytes); + } + SubCommand::Unstack => { + let bytes = SocketMessage::UnstackWindow.as_bytes()?; + send_message(&*bytes); + } + SubCommand::CycleStack(direction) => { + let bytes = SocketMessage::CycleStack(direction).as_bytes()?; + send_message(&*bytes); + } + SubCommand::FlipLayout(flip) => { + let bytes = SocketMessage::FlipLayout(flip).as_bytes()?; + send_message(&*bytes); + } + SubCommand::FocusMonitor(target) => { + let bytes = SocketMessage::FocusMonitorNumber(target.number) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::FocusWorkspace(target) => { + let bytes = SocketMessage::FocusWorkspaceNumber(target.number) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + SubCommand::WorkspaceName(name) => { + let bytes = SocketMessage::WorkspaceName(name.monitor, name.workspace, name.value) + .as_bytes() + .unwrap(); + send_message(&*bytes); + } + } + + Ok(()) +}