mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-15 09:47:40 +01:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
644f7ee604 | ||
|
|
b9a40924a8 | ||
|
|
80bcb51f75 | ||
|
|
e10e11d1de | ||
|
|
2807cafdd0 | ||
|
|
63cf48daa5 | ||
|
|
a2b49845ac | ||
|
|
5b923a135c | ||
|
|
b8a27a93fe | ||
|
|
28a641609c | ||
|
|
f1ee5ea194 | ||
|
|
ff53533da0 | ||
|
|
c4c8bd7d4b | ||
|
|
e1bd0e9fcb | ||
|
|
368d41e3e0 | ||
|
|
2b7c51b87b | ||
|
|
ce3c742e09 | ||
|
|
2a4e6fa6da | ||
|
|
2d19109fb6 | ||
|
|
a9a0ecd49d | ||
|
|
5ec2b80c3a | ||
|
|
4cc059ff1d | ||
|
|
d4d7e2b694 | ||
|
|
84752c4338 | ||
|
|
ffa0786178 | ||
|
|
752dde2288 | ||
|
|
dc771a104d | ||
|
|
e5a7c140ff | ||
|
|
61b231be28 |
1
.github/workflows/windows.yaml
vendored
1
.github/workflows/windows.yaml
vendored
@@ -66,7 +66,6 @@ jobs:
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
|
||||
del rustup-init.exe
|
||||
shell: powershell
|
||||
- name: Ensure stable toolchain is up to date
|
||||
run: rustup update stable
|
||||
|
||||
127
Cargo.lock
generated
127
Cargo.lock
generated
@@ -73,9 +73,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.69"
|
||||
version = "1.0.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
|
||||
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -310,7 +310,7 @@ checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.2.10",
|
||||
"redox_syscall",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
@@ -505,10 +505,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebi"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"bitflags",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-utils",
|
||||
@@ -530,12 +531,13 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"uds_windows",
|
||||
"which",
|
||||
"winput",
|
||||
"winvd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
@@ -547,7 +549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebic"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
@@ -578,15 +580,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.100"
|
||||
version = "0.2.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5"
|
||||
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
|
||||
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
@@ -768,9 +770,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.26.1"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2766204889d09937d00bfbb7fec56bb2a199e2ade963cab19185d8a6104c7c"
|
||||
checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -795,9 +797,9 @@ checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
@@ -806,16 +808,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
|
||||
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"petgraph",
|
||||
"redox_syscall 0.2.10",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"thread-id",
|
||||
"winapi 0.3.9",
|
||||
@@ -881,9 +883,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.28"
|
||||
version = "1.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
|
||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
@@ -999,12 +1001,6 @@ dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
@@ -1021,7 +1017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall 0.2.10",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1059,9 +1055,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49"
|
||||
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1086,18 +1082,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.128"
|
||||
version = "1.0.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1056a0db1978e9dbf0f6e4fca677f6f9143dc1c19de346f22cac23e422196834"
|
||||
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.128"
|
||||
version = "1.0.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13af2fbb8b60a8950d6c72a56d2095c28870367cc8e10c55e9745bac4995a2c4"
|
||||
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1106,9 +1102,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.66"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
|
||||
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -1165,9 +1161,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.75"
|
||||
version = "1.0.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7"
|
||||
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1176,9 +1172,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.20.1"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4786f320388e6031aa78c68455553f4e3425deeeb40565fecba3d101c1faf21f"
|
||||
checksum = "92d77883450d697c0010e60db3d940ed130b0ed81d27485edee981621b434e52"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"core-foundation-sys",
|
||||
@@ -1219,12 +1215,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "3.3.0"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
|
||||
checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.1.57",
|
||||
"redox_syscall",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
@@ -1249,9 +1245,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.26"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
|
||||
checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"pin-project-lite",
|
||||
@@ -1272,9 +1268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2"
|
||||
checksum = "98863d0dd09fa59a1b79c6750ad80dbda6b75f4e71c437a6a1a8cb91a8bcbd77"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1283,9 +1279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8"
|
||||
checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
@@ -1323,9 +1319,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.2.20"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cbe87a2fa7e35900ce5de20220a582a9483a7063811defce79d7cbd59d4cfe"
|
||||
checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"chrono",
|
||||
@@ -1361,9 +1357,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -1456,9 +1452,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef84dd25f4c69a271b1bba394532bf400523b43169de21dfc715e8f8e491053d"
|
||||
checksum = "d7524f6f9074f6326a1c167cd3dc2ed4e6916648a1a55116d029620af9b65fb1"
|
||||
dependencies = [
|
||||
"const-sha1",
|
||||
"windows_gen",
|
||||
@@ -1467,9 +1463,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_gen"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac7bb21b8ff5e801232b72a6ff554b4cc0cef9ed9238188c3ca78fe3968a7e5d"
|
||||
checksum = "4be44a189bde96fc0e0cdd5b152b2d21c635c0c94c7d256aab4425477b2a2f37"
|
||||
dependencies = [
|
||||
"windows_quote",
|
||||
"windows_reader",
|
||||
@@ -1477,9 +1473,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_macros"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5566b8c51118769e4a9094a688bf1233a3f36aacbfc78f3b15817fe0b6e0442f"
|
||||
checksum = "cc1d78ce8a43d45b8da282383a2cb2ffcd5587cc3a9c341125d3181d2b701ede"
|
||||
dependencies = [
|
||||
"syn",
|
||||
"windows_gen",
|
||||
@@ -1489,15 +1485,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_quote"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4af8236a9493c38855f95cdd11b38b342512a5df4ee7473cffa828b5ebb0e39c"
|
||||
checksum = "51fa2185b18a6164a3fa3ea2b6c92ebc1b60f532ae5a85c57408ba6a5a064913"
|
||||
|
||||
[[package]]
|
||||
name = "windows_reader"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c8d5cf83fb08083438c5c46723e6206b2970da57ce314f80b57724439aaacab"
|
||||
checksum = "3daa5bd758f2f8f20cd93a79aedca20759779f43785fc77b08a4e8e1e5876bbb"
|
||||
|
||||
[[package]]
|
||||
name = "winput"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd4bec39938e0ae68b300e2a4197b6437f13d53d1c146c6e297e346a71d5dde9"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winvd"
|
||||
|
||||
62
README.md
62
README.md
@@ -14,6 +14,10 @@ _komorebi_ allows you to control application windows, virtual workspaces and dis
|
||||
used with third-party software such as [AutoHotKey](https://github.com/Lexikos/AutoHotkey_L) to set user-defined
|
||||
keyboard shortcuts.
|
||||
|
||||
Translations of this document can be found in the project wiki:
|
||||
|
||||
- [komorebi 中文用户指南](https://github.com/LGUG2Z/komorebi/wiki/README-zh) (by [@crosstyan](https://github.com/crosstyan))
|
||||
|
||||
## Description
|
||||
|
||||
_komorebi_ only responds to [WinEvents](https://docs.microsoft.com/en-us/windows/win32/winauto/event-constants) and the
|
||||
@@ -160,6 +164,46 @@ komorebic.exe identify-tray-application exe Discord.exe
|
||||
# komorebic.exe identify-tray-application title [TITLE]
|
||||
```
|
||||
|
||||
#### Focus Follows Mouse
|
||||
|
||||
`komorebi` supports two focus-follows-mouse implementations; the native Windows Xmouse implementation, which treats the
|
||||
desktop, the task bar, and the system tray as windows and switches focus to them eagerly, and a custom `komorebi`
|
||||
implementation, which only considers windows managed by `komorebi` as valid targets to switch focus to when moving the
|
||||
mouse.
|
||||
|
||||
To enable the `komorebi` implementation you must start the process with the `--ffm` flag to explicitly enable the feature.
|
||||
This is because the mouse tracking required for this feature significantly increases the CPU usage of the process (on my
|
||||
machine, it jumps from <1% to ~4~), and this CPU increase persists regardless of whether focus-follows-mouse is enabled
|
||||
or disabled at any given time via `komorebic`'s configuration commands.
|
||||
|
||||
When calling any of the `komorebic` commands related to focus-follows-mouse functionality, the `windows`
|
||||
implementation will be chosen as the default implementation. You can optionally specify the `komorebi` implementation by
|
||||
passing it as an argument to the `--implementation` flag:
|
||||
|
||||
```powershell
|
||||
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
|
||||
```
|
||||
|
||||
#### Saving and Loading Resized Layouts
|
||||
|
||||
If you create a BSP layout through various resize adjustments that you want to be able to restore easily in the future,
|
||||
it is possible to "quicksave" that layout to the system's temporary folder and load it later in the same session, or
|
||||
alternatively, you may save it to a specific file to be loaded again at any point in the future.
|
||||
|
||||
```powershell
|
||||
komorebic.exe quick-save # saves the focused workspace to $Env:TEMP\komorebi.quicksave.json
|
||||
komorebic.exe quick-load # loads $Env:TEMP\komorebi.quicksave.json on the focused workspace
|
||||
|
||||
komorebic.exe save ~/layouts/primary.json # saves the focused workspace to $Env:USERPROFILE\layouts\primary.json
|
||||
komorebic.exe load ~/layouts/secondary.json # loads $Env:USERPROFILE\layouts\secondary.json on the focused workspace
|
||||
```
|
||||
|
||||
These layouts can be applied to arbitrary collections of windows on any workspace, as they only track the layout
|
||||
dimensions and are not coupled to the applications that were running at the time of saving.
|
||||
|
||||
When layouts that expect more or less windows than the number currently on the focused workspace are loaded, `komorebi`
|
||||
will automatically reconcile the difference.
|
||||
|
||||
## Configuration with `komorebic`
|
||||
|
||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||
@@ -174,7 +218,12 @@ each command.
|
||||
start Start komorebi.exe as a background process
|
||||
stop Stop the komorebi.exe process and restore all hidden windows
|
||||
state Show a JSON representation of the current window manager state
|
||||
query Query the current window manager state
|
||||
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
quick-save Quicksave the current resize layout dimensions
|
||||
quick-load Load the last quicksaved resize layout dimensions
|
||||
save Save the current resize layout dimensions to a file
|
||||
load Load the resize layout dimensions from a file
|
||||
focus Change focus to the window in the specified direction
|
||||
move Move the focused window in the specified direction
|
||||
stack Stack the focused window in the specified direction
|
||||
@@ -188,6 +237,7 @@ send-to-workspace Send the focused window to the specified workspace
|
||||
focus-monitor Focus the specified monitor
|
||||
focus-workspace Focus the specified workspace on the focused monitor
|
||||
new-workspace Create and append a new workspace on the focused monitor
|
||||
invisible-borders Set the invisible border dimensions around each window
|
||||
adjust-container-padding Adjust container padding on the focused workspace
|
||||
adjust-workspace-padding Adjust workspace padding on the focused workspace
|
||||
change-layout Set the layout on the focused workspace
|
||||
@@ -214,6 +264,7 @@ float-rule Add a rule to always float the specified applicati
|
||||
manage-rule Add a rule to always manage the specified application
|
||||
workspace-rule Add a rule to associate an application with a workspace
|
||||
identify-tray-application Identify an application that closes to the system tray
|
||||
identify-border-overflow Identify an application that has overflowing borders
|
||||
focus-follows-mouse Enable or disable focus follows mouse for the operating system
|
||||
toggle-focus-follows-mouse Toggle focus follows mouse for the operating system
|
||||
ahk-library Generate a library of AutoHotKey helper functions
|
||||
@@ -245,6 +296,8 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Mouse follows focused container
|
||||
- [x] Resize window container in direction
|
||||
- [ ] Resize child window containers by split ratio
|
||||
- [x] Quicksave and quickload layouts with resize dimensions
|
||||
- [x] Save and load layouts with resize dimensions to/from specific files
|
||||
- [x] Mouse drag to swap window container position
|
||||
- [x] Mouse drag to resize window container
|
||||
- [x] Configurable workspace and container gaps
|
||||
@@ -254,11 +307,14 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Floating rules based on exe name, window title and class
|
||||
- [x] Workspace rules based on exe name and window class
|
||||
- [x] Additional manage rules based on exe name and window class
|
||||
- [x] Identify applications which overflow their borders by exe name and class
|
||||
- [x] Identify 'close/minimize to tray' applications by exe name and class
|
||||
- [x] Configure and compensate for the size of Windows 10's invisible borders
|
||||
- [x] Toggle floating windows
|
||||
- [x] Toggle monocle window
|
||||
- [x] Toggle native maximization
|
||||
- [x] Toggle focus follows mouse
|
||||
- [x] Toggle Xmouse/Windows focus follows mouse implementation
|
||||
- [x] Toggle Komorebi focus follows mouse implementation (desktop and system tray-aware)
|
||||
- [x] Toggle automatic tiling
|
||||
- [x] Pause all window management
|
||||
- [x] Load configuration on startup
|
||||
@@ -266,6 +322,7 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Watch configuration for changes
|
||||
- [x] Helper library for AutoHotKey
|
||||
- [x] View window manager state
|
||||
- [x] Query window manager state
|
||||
|
||||
## Development
|
||||
|
||||
@@ -314,8 +371,7 @@ process and the log will appear frozen.
|
||||
|
||||
If you believe you have encountered a deadlock, you can compile `komorebi` with `--features deadlock_detection` and try
|
||||
reproducing the deadlock again. This will check for deadlocks every 5 seconds in the background, and if a deadlock is
|
||||
found, information about it will appear in the log which can be shared when opening an issu which can be shared when
|
||||
opening an issue.
|
||||
found, information about it will appear in the log which can be shared when opening an issue.
|
||||
|
||||
## Window Manager State and Integrations
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
windows = "0.19"
|
||||
windows = "0.20"
|
||||
|
||||
[build-dependencies]
|
||||
windows = "0.19"
|
||||
windows = "0.20"
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
pub use windows::Handle;
|
||||
pub use windows::Result;
|
||||
|
||||
::windows::include_bindings!();
|
||||
|
||||
@@ -8,11 +8,11 @@ use ::std::convert::Into;
|
||||
use ::std::iter::Extend;
|
||||
use ::std::iter::Iterator;
|
||||
use ::std::matches;
|
||||
use ::std::option::Option::Some;
|
||||
use ::std::string::ToString;
|
||||
use ::std::unreachable;
|
||||
|
||||
use ::quote::quote;
|
||||
use ::std::option::Option::Some;
|
||||
use ::syn::parse_macro_input;
|
||||
use ::syn::Data;
|
||||
use ::syn::DataEnum;
|
||||
@@ -20,7 +20,10 @@ use ::syn::DeriveInput;
|
||||
use ::syn::Fields;
|
||||
use ::syn::FieldsNamed;
|
||||
use ::syn::FieldsUnnamed;
|
||||
use ::syn::Meta;
|
||||
use ::syn::NestedMeta;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[proc_macro_derive(AhkFunction)]
|
||||
pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
@@ -29,29 +32,118 @@ pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStre
|
||||
match input.data {
|
||||
Data::Struct(s) => match s.fields {
|
||||
Fields::Named(FieldsNamed { named, .. }) => {
|
||||
let idents = named.iter().map(|f| &f.ident);
|
||||
let arguments = quote! {#(#idents), *}.to_string();
|
||||
let argument_idents = named
|
||||
.iter()
|
||||
// Filter out the flags
|
||||
.filter(|&f| {
|
||||
let mut include = true;
|
||||
for attribute in &f.attrs {
|
||||
if let ::std::result::Result::Ok(Meta::List(list)) =
|
||||
attribute.parse_meta()
|
||||
{
|
||||
for nested in list.nested {
|
||||
if let NestedMeta::Meta(Meta::Path(path)) = nested {
|
||||
if path.is_ident("long") {
|
||||
include = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let idents = named.iter().map(|f| &f.ident);
|
||||
let called_arguments = quote! {#(%#idents%) *}
|
||||
include
|
||||
})
|
||||
.map(|f| &f.ident);
|
||||
|
||||
let argument_idents_clone = argument_idents.clone();
|
||||
|
||||
let called_arguments = quote! {#(%#argument_idents_clone%) *}
|
||||
.to_string()
|
||||
.replace(" %", "%")
|
||||
.replace("% ", "%")
|
||||
.replace("%%", "% %");
|
||||
|
||||
quote! {
|
||||
impl AhkFunction for #name {
|
||||
fn generate_ahk_function() -> String {
|
||||
::std::format!(r#"
|
||||
let flag_idents = named
|
||||
.iter()
|
||||
// Filter only the flags
|
||||
.filter(|f| {
|
||||
let mut include = false;
|
||||
|
||||
for attribute in &f.attrs {
|
||||
if let ::std::result::Result::Ok(Meta::List(list)) =
|
||||
attribute.parse_meta()
|
||||
{
|
||||
for nested in list.nested {
|
||||
if let NestedMeta::Meta(Meta::Path(path)) = nested {
|
||||
// Identify them using the --long flag name
|
||||
if path.is_ident("long") {
|
||||
include = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include
|
||||
})
|
||||
.map(|f| &f.ident);
|
||||
|
||||
let has_flags = flag_idents.clone().count() != 0;
|
||||
|
||||
if has_flags {
|
||||
let flag_idents_concat = flag_idents.clone();
|
||||
let argument_idents_concat = argument_idents.clone();
|
||||
|
||||
// Concat the args and flag args if there are flags
|
||||
let all_arguments =
|
||||
quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *}
|
||||
.to_string();
|
||||
|
||||
let flag_idents_clone = flag_idents.clone();
|
||||
let flags = quote! {#(--#flag_idents_clone) *}
|
||||
.to_string()
|
||||
.replace("- - ", "--");
|
||||
|
||||
let called_flag_arguments = quote! {#(%#flag_idents%) *}
|
||||
.to_string()
|
||||
.replace(" %", "%")
|
||||
.replace("% ", "%")
|
||||
.replace("%%", "% %");
|
||||
|
||||
quote! {
|
||||
impl AhkFunction for #name {
|
||||
fn generate_ahk_function() -> String {
|
||||
::std::format!(r#"
|
||||
{}({}) {{
|
||||
Run, komorebic.exe {} {} {} {}, , Hide
|
||||
}}"#,
|
||||
::std::stringify!(#name),
|
||||
#all_arguments,
|
||||
::std::stringify!(#name).to_kebab_case(),
|
||||
#called_arguments,
|
||||
#flags,
|
||||
#called_flag_arguments
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let arguments = quote! {#(#argument_idents), *}.to_string();
|
||||
|
||||
quote! {
|
||||
impl AhkFunction for #name {
|
||||
fn generate_ahk_function() -> String {
|
||||
::std::format!(r#"
|
||||
{}({}) {{
|
||||
Run, komorebic.exe {} {}, , Hide
|
||||
}}"#,
|
||||
::std::stringify!(#name),
|
||||
#arguments,
|
||||
stringify!(#name).to_kebab_case(),
|
||||
#called_arguments
|
||||
)
|
||||
}
|
||||
::std::stringify!(#name),
|
||||
#arguments,
|
||||
::std::stringify!(#name).to_kebab_case(),
|
||||
#called_arguments
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ArgEnum;
|
||||
@@ -52,6 +53,10 @@ pub enum SocketMessage {
|
||||
Stop,
|
||||
TogglePause,
|
||||
Retile,
|
||||
QuickSave,
|
||||
QuickLoad,
|
||||
Save(PathBuf),
|
||||
Load(PathBuf),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusWorkspaceNumber(usize),
|
||||
ContainerPadding(usize, usize, i32),
|
||||
@@ -62,13 +67,16 @@ pub enum SocketMessage {
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
WatchConfiguration(bool),
|
||||
InvisibleBorders(Rect),
|
||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||
IdentifyBorderOverflow(ApplicationIdentifier, String),
|
||||
State,
|
||||
FocusFollowsMouse(bool),
|
||||
ToggleFocusFollowsMouse,
|
||||
Query(StateQuery),
|
||||
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
|
||||
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
@@ -89,6 +97,15 @@ impl FromStr for SocketMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum StateQuery {
|
||||
FocusedMonitorIndex,
|
||||
FocusedWorkspaceIndex,
|
||||
FocusedContainerIndex,
|
||||
FocusedWindowIndex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ApplicationIdentifier {
|
||||
@@ -97,6 +114,13 @@ pub enum ApplicationIdentifier {
|
||||
Title,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
Komorebi,
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Sizing {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use bindings::Windows::Win32::Foundation::RECT;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Rect {
|
||||
pub left: i32,
|
||||
pub top: i32,
|
||||
@@ -10,17 +11,6 @@ pub struct Rect {
|
||||
pub bottom: i32,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RECT> for Rect {
|
||||
fn from(rect: RECT) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
; Enable hot reloading of changes to this file
|
||||
Run, komorebic.exe watch-configuration enable, , Hide
|
||||
|
||||
; Configure the invisible border dimensions
|
||||
Run, komorebic.exe invisible-borders 7 0 14 7, , Hide
|
||||
|
||||
; Enable focus follows mouse
|
||||
Run, komorebic.exe focus-follows-mouse enable, , Hide
|
||||
|
||||
@@ -52,6 +55,9 @@ Run, komorebic.exe manage-rule exe TIM.exe, , Hide
|
||||
; Identify applications that close to the tray
|
||||
Run, komorebic.exe identify-tray-application exe Discord.exe, , Hide
|
||||
|
||||
; Identify applications that have overflowing borders
|
||||
Run, komorebic.exe identify-border-overflow exe Discord.exe, , Hide
|
||||
|
||||
; Change the focused window, Alt + Vim direction keys
|
||||
!h::
|
||||
Run, komorebic.exe focus left, , Hide
|
||||
|
||||
@@ -7,6 +7,9 @@ WatchConfiguration("enable")
|
||||
; Ensure there are 5 workspaces created on monitor 0
|
||||
EnsureWorkspaces(0, 5)
|
||||
|
||||
; Configure the invisible border dimensions
|
||||
InvisibleBorders(7, 0, 14, 7)
|
||||
|
||||
; Configure the 1st workspace
|
||||
WorkspaceName(0, 0, "bsp")
|
||||
|
||||
@@ -37,9 +40,23 @@ FloatRule("exe", "Wally.exe")
|
||||
FloatRule("exe", "wincompose.exe")
|
||||
FloatRule("exe", "1Password.exe")
|
||||
FloatRule("exe", "Wox.exe")
|
||||
FloatRule("exe", "ddm.exe")
|
||||
FloatRule("class", "Chrome_RenderWidgetHostHWND") ; GOG Electron invisible overlay
|
||||
FloatRule("class", "CEFCLIENT")
|
||||
|
||||
; Identify Minimize-to-Tray Applications
|
||||
IdentifyTrayApplication("exe", "Discord.exe")
|
||||
IdentifyTrayApplication("exe", "Spotify.exe")
|
||||
IdentifyTrayApplication("exe", "GalaxyClient.exe")
|
||||
|
||||
; Identify Electron applications with overflowing borders
|
||||
IdentifyBorderOverflow("exe", "Discord.exe")
|
||||
IdentifyBorderOverflow("exe", "Spotify.exe")
|
||||
IdentifyBorderOverflow("exe", "GalaxyClient.exe")
|
||||
IdentifyBorderOverflow("class", "ZPFTEWndClass")
|
||||
|
||||
; Identify applications to be forcibly managed
|
||||
ManageRule("exe", "GalaxyClient.exe")
|
||||
|
||||
; Change the focused window, Alt + Vim direction keys
|
||||
!h::
|
||||
@@ -170,9 +187,9 @@ return
|
||||
TogglePause()
|
||||
return
|
||||
|
||||
; Toggle focus follows mouse
|
||||
; Enable focus follows mouse
|
||||
!0::
|
||||
ToggleFocusFollowsMouse()
|
||||
ToggleFocusFollowsMouse("komorebi")
|
||||
return
|
||||
|
||||
; Switch to workspace
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -10,6 +15,7 @@ bindings = { package = "bindings", path = "../bindings" }
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
|
||||
bitflags = "1"
|
||||
clap = "3.0.0-beta.4"
|
||||
color-eyre = "0.5"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
@@ -30,6 +36,7 @@ tracing-appender = "0.1"
|
||||
tracing-subscriber = "0.2"
|
||||
uds_windows = "1"
|
||||
which = "4"
|
||||
winput = "0.2"
|
||||
winvd = "0.0.20"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -44,6 +44,18 @@ impl Container {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||
for window in self.windows() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||
for window in self.windows() {
|
||||
if window.hwnd == hwnd {
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use std::thread;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Clap;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
@@ -25,6 +28,7 @@ use which::which;
|
||||
|
||||
use crate::process_command::listen_for_commands;
|
||||
use crate::process_event::listen_for_events;
|
||||
use crate::process_movement::listen_for_movements;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
@@ -36,6 +40,7 @@ mod container;
|
||||
mod monitor;
|
||||
mod process_command;
|
||||
mod process_event;
|
||||
mod process_movement;
|
||||
mod set_window_position;
|
||||
mod styles;
|
||||
mod window;
|
||||
@@ -51,16 +56,15 @@ lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_EXE_WHITELIST: Arc<Mutex<Vec<String>>> =
|
||||
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_CLASSES: Arc<Mutex<Vec<String>>> =
|
||||
Arc::new(Mutex::new(vec![]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"explorer.exe".to_string(),
|
||||
"firefox.exe".to_string(),
|
||||
"chrome.exe".to_string(),
|
||||
"idea64.exe".to_string(),
|
||||
"ApplicationFrameHost.exe".to_string(),
|
||||
"steam.exe".to_string(),
|
||||
]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
|
||||
Arc::new(Mutex::new(vec![
|
||||
"explorer.exe".to_string(),
|
||||
"firefox.exe".to_string(),
|
||||
"chrome.exe".to_string(),
|
||||
"idea64.exe".to_string(),
|
||||
"ApplicationFrameHost.exe".to_string(),
|
||||
"steam.exe".to_string(),
|
||||
]));
|
||||
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"firefox.exe".to_string(),
|
||||
"idea64.exe".to_string(),
|
||||
@@ -69,8 +73,11 @@ lazy_static! {
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
}
|
||||
|
||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
@@ -193,61 +200,76 @@ fn detect_deadlocks() {
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(long = "ffm")]
|
||||
focus_follows_mouse: bool,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn main() -> Result<()> {
|
||||
match std::env::args().count() {
|
||||
1 => {
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes();
|
||||
let opts: Opts = Opts::parse();
|
||||
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
|
||||
|
||||
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);
|
||||
}
|
||||
let arg_count = std::env::args().count();
|
||||
let has_valid_args = arg_count == 1 || (arg_count == 2 && CUSTOM_FFM.load(Ordering::SeqCst));
|
||||
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
if has_valid_args {
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes();
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
let process_id = WindowsApi::current_process_id();
|
||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||
|
||||
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
crossbeam_channel::unbounded();
|
||||
|
||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||
winevent_listener.start();
|
||||
|
||||
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||
incoming,
|
||||
)))?));
|
||||
|
||||
wm.lock().init()?;
|
||||
listen_for_commands(wm.clone());
|
||||
listen_for_events(wm.clone());
|
||||
|
||||
load_configuration()?;
|
||||
|
||||
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().restore_all_windows();
|
||||
std::process::exit(130);
|
||||
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);
|
||||
}
|
||||
_ => Ok(()),
|
||||
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
let process_id = WindowsApi::current_process_id();
|
||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||
|
||||
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
crossbeam_channel::unbounded();
|
||||
|
||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||
winevent_listener.start();
|
||||
|
||||
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||
incoming,
|
||||
)))?));
|
||||
|
||||
wm.lock().init()?;
|
||||
listen_for_commands(wm.clone());
|
||||
listen_for_events(wm.clone());
|
||||
|
||||
if CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||
listen_for_movements(wm.clone());
|
||||
}
|
||||
|
||||
load_configuration()?;
|
||||
|
||||
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().restore_all_windows();
|
||||
std::process::exit(130);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use color_eyre::Result;
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
@@ -14,12 +15,13 @@ use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters)]
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
id: isize,
|
||||
monitor_size: Rect,
|
||||
#[getset(get = "pub")]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
size: Rect,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
work_area_size: Rect,
|
||||
workspaces: Ring<Workspace>,
|
||||
#[serde(skip_serializing)]
|
||||
@@ -29,12 +31,15 @@ pub struct Monitor {
|
||||
|
||||
impl_ring_elements!(Monitor, Workspace);
|
||||
|
||||
pub fn new(id: isize, monitor_size: Rect, work_area_size: Rect) -> Monitor {
|
||||
pub fn new(id: isize, size: Rect, work_area_size: Rect) -> Monitor {
|
||||
let mut workspaces = Ring::default();
|
||||
workspaces.elements_mut().push_back(Workspace::default());
|
||||
|
||||
Monitor {
|
||||
id,
|
||||
monitor_size,
|
||||
size,
|
||||
work_area_size,
|
||||
workspaces: Ring::default(),
|
||||
workspaces,
|
||||
workspace_names: HashMap::default(),
|
||||
}
|
||||
}
|
||||
@@ -141,12 +146,12 @@ impl Monitor {
|
||||
self.workspaces().len()
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace(&mut self) -> Result<()> {
|
||||
pub fn update_focused_workspace(&mut self, invisible_borders: &Rect) -> Result<()> {
|
||||
let work_area = *self.work_area_size();
|
||||
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.update(&work_area)?;
|
||||
.update(&work_area, invisible_borders)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
@@ -10,16 +13,19 @@ use color_eyre::Result;
|
||||
use parking_lot::Mutex;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::StateQuery;
|
||||
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -113,7 +119,12 @@ impl WindowManager {
|
||||
self.move_container_to_monitor(monitor_idx, false)?;
|
||||
}
|
||||
SocketMessage::TogglePause => {
|
||||
tracing::info!("pausing");
|
||||
if self.is_paused {
|
||||
tracing::info!("resuming");
|
||||
} else {
|
||||
tracing::info!("pausing");
|
||||
}
|
||||
|
||||
self.is_paused = !self.is_paused;
|
||||
}
|
||||
SocketMessage::ToggleTiling => {
|
||||
@@ -123,21 +134,7 @@ impl WindowManager {
|
||||
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();
|
||||
let workspace = monitor
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||
|
||||
// Reset any resize adjustments if we want to force a retile
|
||||
for resize in workspace.resize_dimensions_mut() {
|
||||
*resize = None;
|
||||
}
|
||||
|
||||
workspace.update(&work_area)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::Retile => self.retile_all()?,
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?,
|
||||
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
|
||||
@@ -147,6 +144,15 @@ impl WindowManager {
|
||||
self.set_workspace_layout(monitor_idx, workspace_idx, layout)?;
|
||||
}
|
||||
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
let monitor_idx = self.monitor_idx_from_current_pos().ok_or_else(|| {
|
||||
anyhow!("there is no monitor associated with the current cursor position")
|
||||
})?;
|
||||
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
SocketMessage::Stop => {
|
||||
@@ -175,21 +181,124 @@ impl WindowManager {
|
||||
let mut stream = UnixStream::connect(&socket)?;
|
||||
stream.write_all(state.as_bytes())?;
|
||||
}
|
||||
SocketMessage::Query(query) => {
|
||||
let response = match query {
|
||||
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
|
||||
StateQuery::FocusedWorkspaceIndex => self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.focused_workspace_idx(),
|
||||
StateQuery::FocusedContainerIndex => {
|
||||
self.focused_workspace()?.focused_container_idx()
|
||||
}
|
||||
StateQuery::FocusedWindowIndex => {
|
||||
self.focused_container()?.focused_window_idx()
|
||||
}
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let mut socket =
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
let mut stream = UnixStream::connect(&socket)?;
|
||||
stream.write_all(response.as_bytes())?;
|
||||
}
|
||||
SocketMessage::ResizeWindow(direction, sizing) => {
|
||||
self.resize_window(direction, sizing, Option::from(50))?;
|
||||
}
|
||||
SocketMessage::FocusFollowsMouse(enable) => {
|
||||
if enable {
|
||||
WindowsApi::enable_focus_follows_mouse()?;
|
||||
} else {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
|
||||
if !CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||
tracing::warn!(
|
||||
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be enabled; defaulting to windows implementation"
|
||||
);
|
||||
implementation = FocusFollowsMouseImplementation::Windows;
|
||||
}
|
||||
|
||||
match implementation {
|
||||
FocusFollowsMouseImplementation::Komorebi => {
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
tracing::warn!(
|
||||
"the komorebi implementation of focus follows mouse cannot be enabled while the windows implementation is enabled"
|
||||
);
|
||||
} else if enable {
|
||||
self.focus_follows_mouse = Option::from(implementation);
|
||||
} else {
|
||||
self.focus_follows_mouse = None;
|
||||
self.has_pending_raise_op = false;
|
||||
}
|
||||
}
|
||||
FocusFollowsMouseImplementation::Windows => {
|
||||
if let Some(FocusFollowsMouseImplementation::Komorebi) =
|
||||
self.focus_follows_mouse
|
||||
{
|
||||
tracing::warn!(
|
||||
"the windows implementation of focus follows mouse cannot be enabled while the komorebi implementation is enabled"
|
||||
);
|
||||
} else if enable {
|
||||
WindowsApi::enable_focus_follows_mouse()?;
|
||||
self.focus_follows_mouse =
|
||||
Option::from(FocusFollowsMouseImplementation::Windows);
|
||||
} else {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
self.focus_follows_mouse = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleFocusFollowsMouse => {
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
} else {
|
||||
WindowsApi::enable_focus_follows_mouse()?;
|
||||
SocketMessage::ToggleFocusFollowsMouse(mut implementation) => {
|
||||
if !CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||
tracing::warn!(
|
||||
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be toggled; defaulting to windows implementation"
|
||||
);
|
||||
implementation = FocusFollowsMouseImplementation::Windows;
|
||||
}
|
||||
|
||||
match implementation {
|
||||
FocusFollowsMouseImplementation::Komorebi => {
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
tracing::warn!(
|
||||
"the komorebi implementation of focus follows mouse cannot be toggled while the windows implementation is enabled"
|
||||
);
|
||||
} else {
|
||||
match self.focus_follows_mouse {
|
||||
None => {
|
||||
self.focus_follows_mouse = Option::from(implementation);
|
||||
self.has_pending_raise_op = false;
|
||||
}
|
||||
Some(FocusFollowsMouseImplementation::Komorebi) => {
|
||||
self.focus_follows_mouse = None;
|
||||
}
|
||||
Some(FocusFollowsMouseImplementation::Windows) => {
|
||||
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FocusFollowsMouseImplementation::Windows => {
|
||||
if let Some(FocusFollowsMouseImplementation::Komorebi) =
|
||||
self.focus_follows_mouse
|
||||
{
|
||||
tracing::warn!(
|
||||
"the windows implementation of focus follows mouse cannot be toggled while the komorebi implementation is enabled"
|
||||
);
|
||||
} else {
|
||||
match self.focus_follows_mouse {
|
||||
None => {
|
||||
WindowsApi::enable_focus_follows_mouse()?;
|
||||
self.focus_follows_mouse = Option::from(implementation);
|
||||
}
|
||||
Some(FocusFollowsMouseImplementation::Windows) => {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
self.focus_follows_mouse = None;
|
||||
}
|
||||
Some(FocusFollowsMouseImplementation::Komorebi) => {
|
||||
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ReloadConfiguration => {
|
||||
@@ -198,28 +307,85 @@ impl WindowManager {
|
||||
SocketMessage::WatchConfiguration(enable) => {
|
||||
self.watch_configuration(enable)?;
|
||||
}
|
||||
SocketMessage::IdentifyTrayApplication(identifier, id) => match identifier {
|
||||
ApplicationIdentifier::Exe => {
|
||||
let mut exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
|
||||
if !exes.contains(&id) {
|
||||
exes.push(id);
|
||||
}
|
||||
SocketMessage::IdentifyBorderOverflow(_, id) => {
|
||||
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
if !identifiers.contains(&id) {
|
||||
identifiers.push(id);
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
let mut classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
|
||||
if !classes.contains(&id) {
|
||||
classes.push(id);
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyTrayApplication(_, id) => {
|
||||
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
if !identifiers.contains(&id) {
|
||||
identifiers.push(id);
|
||||
}
|
||||
ApplicationIdentifier::Title => {}
|
||||
},
|
||||
}
|
||||
SocketMessage::ManageFocusedWindow => {
|
||||
self.manage_focused_window()?;
|
||||
}
|
||||
SocketMessage::UnmanageFocusedWindow => {
|
||||
self.unmanage_focused_window()?;
|
||||
}
|
||||
}
|
||||
SocketMessage::InvisibleBorders(rect) => {
|
||||
self.invisible_borders = rect;
|
||||
self.retile_all()?;
|
||||
}
|
||||
SocketMessage::QuickSave => {
|
||||
let workspace = self.focused_workspace()?;
|
||||
let resize = workspace.resize_dimensions();
|
||||
|
||||
let mut quicksave_json = std::env::temp_dir();
|
||||
quicksave_json.push("komorebi.quicksave.json");
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(quicksave_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &resize)?;
|
||||
}
|
||||
SocketMessage::QuickLoad => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let mut quicksave_json = std::env::temp_dir();
|
||||
quicksave_json.push("komorebi.quicksave.json");
|
||||
|
||||
let file = File::open(&quicksave_json).map_err(|_| {
|
||||
anyhow!(
|
||||
"no quicksave found at {}",
|
||||
quicksave_json.display().to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
SocketMessage::Save(path) => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let resize = workspace.resize_dimensions();
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(path)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &resize)?;
|
||||
}
|
||||
SocketMessage::Load(path) => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let file = File::open(&path)
|
||||
.map_err(|_| anyhow!("no file found at {}", path.display().to_string()))?;
|
||||
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("processed");
|
||||
Ok(())
|
||||
@@ -232,14 +398,15 @@ impl WindowManager {
|
||||
let message = SocketMessage::from_str(&line?)?;
|
||||
|
||||
if self.is_paused {
|
||||
if let SocketMessage::TogglePause = message {
|
||||
tracing::info!("resuming");
|
||||
self.is_paused = !self.is_paused;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::trace!("ignoring while paused");
|
||||
return Ok(());
|
||||
return match message {
|
||||
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||
Ok(self.process_command(message)?)
|
||||
}
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
self.process_command(message)?;
|
||||
|
||||
@@ -15,8 +15,7 @@ use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
@@ -52,11 +51,13 @@ impl WindowManager {
|
||||
|
||||
// Make sure we have the most recently focused monitor from any event
|
||||
match event {
|
||||
WindowManagerEvent::FocusChange(_, window)
|
||||
WindowManagerEvent::MonitorPoll(_, window)
|
||||
| WindowManagerEvent::FocusChange(_, window)
|
||||
| WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||
let monitor_idx = self
|
||||
.monitor_idx_from_window(*window)
|
||||
self.reconcile_monitors()?;
|
||||
|
||||
let monitor_idx = self.monitor_idx_from_window(*window)
|
||||
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
|
||||
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
@@ -64,12 +65,14 @@ impl WindowManager {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||
let work_area = *monitor.work_area_size();
|
||||
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)?;
|
||||
workspace.update(&work_area, &invisible_borders)?;
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
@@ -91,6 +94,10 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
match event {
|
||||
WindowManagerEvent::Raise(window) => {
|
||||
window.raise()?;
|
||||
self.has_pending_raise_op = false;
|
||||
}
|
||||
WindowManagerEvent::Minimize(_, window)
|
||||
| WindowManagerEvent::Destroy(_, window)
|
||||
| WindowManagerEvent::Unmanage(window) => {
|
||||
@@ -105,15 +112,16 @@ impl WindowManager {
|
||||
// and will have is_window() return true, as the process is still running even if
|
||||
// the window is not visible.
|
||||
{
|
||||
let tray_and_multi_window_exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
|
||||
let tray_and_multi_window_classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
|
||||
let tray_and_multi_window_identifiers =
|
||||
TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
|
||||
// We don't want to purge windows that have been deliberately hidden by us, eg. when
|
||||
// they are not on the top of a container stack.
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
|
||||
if (!window.is_window() || tray_and_multi_window_exes.contains(&window.exe()?))
|
||||
|| tray_and_multi_window_classes.contains(&window.class()?)
|
||||
if (!window.is_window()
|
||||
|| tray_and_multi_window_identifiers.contains(&window.exe()?))
|
||||
|| tray_and_multi_window_identifiers.contains(&window.class()?)
|
||||
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
{
|
||||
hide = true;
|
||||
@@ -213,19 +221,11 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no latest layout"))?;
|
||||
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||
|
||||
// See Window.set_position() in window.rs for comments
|
||||
let border = Rect {
|
||||
left: 12,
|
||||
top: 0,
|
||||
right: 24,
|
||||
bottom: 12,
|
||||
};
|
||||
|
||||
// Adjust for the invisible border
|
||||
new_position.left += border.left;
|
||||
new_position.top += border.top;
|
||||
new_position.right -= border.right;
|
||||
new_position.bottom -= border.bottom;
|
||||
// Adjust for the invisible borders
|
||||
new_position.left += invisible_borders.left;
|
||||
new_position.top += invisible_borders.top;
|
||||
new_position.right -= invisible_borders.right;
|
||||
new_position.bottom -= invisible_borders.bottom;
|
||||
|
||||
let resize = Rect {
|
||||
left: new_position.left - old_position.left,
|
||||
@@ -285,12 +285,12 @@ impl WindowManager {
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::MouseCapture(..) => {}
|
||||
WindowManagerEvent::MonitorPoll(..) | WindowManagerEvent::MouseCapture(..) => {}
|
||||
};
|
||||
|
||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||
if let WindowManagerEvent::Unmanage(window) = event {
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
window.center(&self.focused_monitor_work_area()?, &invisible_borders)?;
|
||||
}
|
||||
|
||||
tracing::trace!("updating list of known hwnds");
|
||||
@@ -315,6 +315,7 @@ impl WindowManager {
|
||||
.open(hwnd_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||
|
||||
tracing::info!("processed: {}", event.window().to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
41
komorebi/src/process_movement.rs
Normal file
41
komorebi/src/process_movement.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use winput::message_loop;
|
||||
use winput::message_loop::Event;
|
||||
use winput::Action;
|
||||
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
|
||||
use crate::window_manager::WindowManager;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || {
|
||||
let mut ignore_movement = false;
|
||||
|
||||
let receiver = message_loop::start().expect("could not start winput message loop");
|
||||
|
||||
loop {
|
||||
let focus_follows_mouse = wm.lock().focus_follows_mouse.clone();
|
||||
if let Some(FocusFollowsMouseImplementation::Komorebi) = focus_follows_mouse {
|
||||
match receiver.next_event() {
|
||||
// Don't want to send any raise events while we are dragging or resizing
|
||||
Event::MouseButton { action, .. } => match action {
|
||||
Action::Press => ignore_movement = true,
|
||||
Action::Release => ignore_movement = false,
|
||||
},
|
||||
Event::MouseMoveRelative { .. } => {
|
||||
if !ignore_movement {
|
||||
match wm.lock().raise_window_at_cursor_pos() {
|
||||
Ok(_) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use crate::styles::GwlExStyle;
|
||||
use crate::styles::GwlStyle;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
@@ -70,7 +71,7 @@ impl Window {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
pub fn center(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
|
||||
@@ -81,44 +82,35 @@ impl Window {
|
||||
right: half_width,
|
||||
bottom: half_weight,
|
||||
},
|
||||
invisible_borders,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
|
||||
// NOTE: This is how the border variable below was calculated; every time this code was
|
||||
// run on any window in any position, the generated border was always the same, so I am
|
||||
// hard coding the border Rect to avoid two calls to set_window_pos and making the screen
|
||||
// flicker on container/window movement. Still not 100% sure if this is DPI-aware.
|
||||
|
||||
// Set the new position first to be able to get the extended frame bounds
|
||||
// WindowsApi::set_window_pos(self.hwnd(), layout, false, false)?;
|
||||
// let mut rect = WindowsApi::window_rect(self.hwnd())?;
|
||||
|
||||
// Get the extended frame bounds of the new position
|
||||
// let frame = WindowsApi::window_rect_with_extended_frame_bounds(self.hwnd())?;
|
||||
|
||||
// Calculate the invisible border diff
|
||||
// let border = Rect {
|
||||
// left: frame.left - rect.left,
|
||||
// top: frame.top - rect.top,
|
||||
// right: rect.right - frame.right,
|
||||
// bottom: rect.bottom - frame.bottom,
|
||||
// };
|
||||
|
||||
pub fn set_position(
|
||||
&mut self,
|
||||
layout: &Rect,
|
||||
invisible_borders: &Rect,
|
||||
top: bool,
|
||||
) -> Result<()> {
|
||||
let mut rect = *layout;
|
||||
let border = Rect {
|
||||
left: 12,
|
||||
top: 0,
|
||||
right: 24,
|
||||
bottom: 12,
|
||||
};
|
||||
let mut should_remove_border = true;
|
||||
|
||||
// Remove the invisible border
|
||||
rect.left -= border.left;
|
||||
rect.top -= border.top;
|
||||
rect.right += border.right;
|
||||
rect.bottom += border.bottom;
|
||||
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
if border_overflows.contains(&self.title()?)
|
||||
|| border_overflows.contains(&self.exe()?)
|
||||
|| border_overflows.contains(&self.class()?)
|
||||
{
|
||||
should_remove_border = false;
|
||||
}
|
||||
|
||||
if should_remove_border {
|
||||
// Remove the invisible borders
|
||||
rect.left -= invisible_borders.left;
|
||||
rect.top -= invisible_borders.top;
|
||||
rect.right += invisible_borders.right;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
}
|
||||
|
||||
WindowsApi::position_window(self.hwnd(), &rect, top)
|
||||
}
|
||||
@@ -156,6 +148,27 @@ impl Window {
|
||||
WindowsApi::maximize_window(self.hwnd());
|
||||
}
|
||||
|
||||
pub fn raise(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
|
||||
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not set as foreground window, but continuing execution of focus(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// This isn't really needed when the above command works as expected via AHK
|
||||
WindowsApi::set_focus(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn focus(self) -> Result<()> {
|
||||
// Attach komorebi thread to Window thread
|
||||
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
@@ -218,6 +231,10 @@ impl Window {
|
||||
|
||||
#[tracing::instrument(fields(exe, title))]
|
||||
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
|
||||
if let Some(WindowManagerEvent::MonitorPoll(_, _)) = event {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if self.title().is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
@@ -17,6 +16,7 @@ use uds_windows::UnixListener;
|
||||
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
@@ -31,11 +31,11 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -44,19 +44,25 @@ pub struct WindowManager {
|
||||
pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>,
|
||||
pub command_listener: UnixListener,
|
||||
pub is_paused: bool,
|
||||
pub invisible_borders: Rect,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub hotwatch: Hotwatch,
|
||||
pub virtual_desktop_id: Option<usize>,
|
||||
pub has_pending_raise_op: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub is_paused: bool,
|
||||
pub invisible_borders: Rect,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub float_identifiers: Vec<String>,
|
||||
pub manage_identifiers: Vec<String>,
|
||||
pub layered_exe_whitelist: Vec<String>,
|
||||
pub tray_and_multi_window_exes: Vec<String>,
|
||||
pub tray_and_multi_window_classes: Vec<String>,
|
||||
pub tray_and_multi_window_identifiers: Vec<String>,
|
||||
pub border_overflow_identifiers: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
@@ -65,11 +71,14 @@ impl From<&mut WindowManager> for State {
|
||||
Self {
|
||||
monitors: wm.monitors.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
invisible_borders: wm.invisible_borders,
|
||||
focus_follows_mouse: wm.focus_follows_mouse.clone(),
|
||||
has_pending_raise_op: wm.has_pending_raise_op,
|
||||
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().clone(),
|
||||
tray_and_multi_window_exes: TRAY_AND_MULTI_WINDOW_EXES.lock().clone(),
|
||||
tray_and_multi_window_classes: TRAY_AND_MULTI_WINDOW_CLASSES.lock().clone(),
|
||||
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
|
||||
border_overflow_identifiers: BORDER_OVERFLOW_IDENTIFIERS.lock().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,8 +137,16 @@ impl WindowManager {
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
invisible_borders: Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
},
|
||||
focus_follows_mouse: None,
|
||||
hotwatch: Hotwatch::new()?,
|
||||
virtual_desktop_id,
|
||||
has_pending_raise_op: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,6 +232,77 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn reconcile_monitors(&mut self) -> Result<()> {
|
||||
let valid_hmonitors = WindowsApi::valid_hmonitors()?;
|
||||
let mut invalid = vec![];
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
if !valid_hmonitors.contains(&monitor.id()) {
|
||||
let mut mark_as_invalid = true;
|
||||
|
||||
// If an invalid hmonitor has at least one window in the window manager state,
|
||||
// we can attempt to update its hmonitor id in-place so that it doesn't get reaped
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if let Some(container) = workspace.focused_container() {
|
||||
if let Some(window) = container.focused_window() {
|
||||
let actual_hmonitor = WindowsApi::monitor_from_window(window.hwnd());
|
||||
if actual_hmonitor != monitor.id() {
|
||||
monitor.set_id(actual_hmonitor);
|
||||
mark_as_invalid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mark_as_invalid {
|
||||
invalid.push(monitor.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any invalid monitors from our state
|
||||
self.monitors_mut().retain(|m| !invalid.contains(&m.id()));
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
let mut should_update = false;
|
||||
let reference = WindowsApi::monitor(monitor.id())?;
|
||||
// TODO: If this is different, force a redraw
|
||||
|
||||
if reference.work_area_size() != monitor.work_area_size() {
|
||||
monitor.set_work_area_size(Rect {
|
||||
left: reference.work_area_size().left,
|
||||
top: reference.work_area_size().top,
|
||||
right: reference.work_area_size().right,
|
||||
bottom: reference.work_area_size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
}
|
||||
if reference.size() != monitor.size() {
|
||||
monitor.set_size(Rect {
|
||||
left: reference.size().left,
|
||||
top: reference.size().top,
|
||||
right: reference.size().right,
|
||||
bottom: reference.size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
}
|
||||
|
||||
if should_update {
|
||||
monitor.update_focused_workspace(&invisible_borders)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for and add any new monitors that may have been plugged in
|
||||
WindowsApi::load_monitor_information(&mut self.monitors)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn enforce_workspace_rules(&mut self) -> Result<()> {
|
||||
let mut to_move = vec![];
|
||||
@@ -332,6 +420,26 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn retile_all(&mut self) -> Result<()> {
|
||||
let invisible_borders = self.invisible_borders;
|
||||
for monitor in self.monitors_mut() {
|
||||
let work_area = *monitor.work_area_size();
|
||||
let workspace = monitor
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||
|
||||
// Reset any resize adjustments if we want to force a retile
|
||||
for resize in workspace.resize_dimensions_mut() {
|
||||
*resize = None;
|
||||
}
|
||||
|
||||
workspace.update(&work_area, &invisible_borders)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn validate_virtual_desktop_id(&self) {
|
||||
let virtual_desktop_id = winvd::helpers::get_current_desktop_number().ok();
|
||||
@@ -360,13 +468,76 @@ impl WindowManager {
|
||||
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> {
|
||||
let mut hwnd = WindowsApi::window_at_cursor_pos()?;
|
||||
|
||||
if self.has_pending_raise_op
|
||||
|| self.focused_window()?.hwnd == hwnd
|
||||
// Sometimes we need this check, because the focus may have been given by a click
|
||||
// to a non-window such as the taskbar or system tray, and komorebi doesn't know that
|
||||
// the focused window of the workspace is not actually focused by the OS at that point
|
||||
|| WindowsApi::foreground_window()? == hwnd
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
let mut known_hwnd = false;
|
||||
for monitor in self.monitors() {
|
||||
for workspace in monitor.workspaces() {
|
||||
if workspace.contains_window(hwnd) {
|
||||
known_hwnd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Not sure if this needs to be made configurable just yet...
|
||||
let overlay_classes = [
|
||||
// Chromium/Electron
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
// Explorer
|
||||
"DirectUIHWND".to_string(),
|
||||
"SysTreeView32".to_string(),
|
||||
"ToolbarWindow32".to_string(),
|
||||
"NetUIHWND".to_string(),
|
||||
];
|
||||
|
||||
if !known_hwnd {
|
||||
let class = Window { hwnd }.class()?;
|
||||
// Some applications (Electron/Chromium-based, explorer) have (invisible?) overlays
|
||||
// windows that we need to look beyond to find the actual window to raise
|
||||
if overlay_classes.contains(&class) {
|
||||
for monitor in self.monitors() {
|
||||
for workspace in monitor.workspaces() {
|
||||
if let Some(exe_hwnd) = workspace.hwnd_from_exe(&Window { hwnd }.exe()?)
|
||||
{
|
||||
hwnd = exe_hwnd;
|
||||
known_hwnd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if known_hwnd {
|
||||
let event = WindowManagerEvent::Raise(Window { hwnd });
|
||||
self.has_pending_raise_op = true;
|
||||
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
|
||||
} else {
|
||||
tracing::debug!("not raising unknown window: {}", Window { hwnd });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn update_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
tracing::info!("updating");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.update_focused_workspace()?;
|
||||
.update_focused_workspace(&invisible_borders)?;
|
||||
|
||||
if mouse_follows_focus {
|
||||
if let Some(window) = self.focused_workspace()?.maximized_window() {
|
||||
@@ -419,9 +590,9 @@ impl WindowManager {
|
||||
) {
|
||||
let unaltered = workspace.layout().calculate(
|
||||
&work_area,
|
||||
NonZeroUsize::new(len).context(
|
||||
"there must be at least one container to calculate a workspace layout",
|
||||
)?,
|
||||
NonZeroUsize::new(len).ok_or_else(|| {
|
||||
anyhow!("there must be at least one container to calculate a workspace layout")
|
||||
})?,
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
@@ -488,6 +659,8 @@ impl WindowManager {
|
||||
pub fn move_container_to_monitor(&mut self, idx: usize, follow: bool) -> Result<()> {
|
||||
tracing::info!("moving container");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
let monitor = self
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
@@ -512,6 +685,7 @@ impl WindowManager {
|
||||
|
||||
target_monitor.add_container(container)?;
|
||||
target_monitor.load_focused_workspace()?;
|
||||
target_monitor.update_focused_workspace(&invisible_borders)?;
|
||||
|
||||
if follow {
|
||||
self.focus_monitor(idx)?;
|
||||
@@ -673,6 +847,7 @@ impl WindowManager {
|
||||
tracing::info!("floating window");
|
||||
|
||||
let work_area = self.focused_monitor_work_area()?;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.new_floating_window()?;
|
||||
@@ -682,7 +857,7 @@ impl WindowManager {
|
||||
.last_mut()
|
||||
.ok_or_else(|| anyhow!("there is no floating window"))?;
|
||||
|
||||
window.center(&work_area)?;
|
||||
window.center(&work_area, &invisible_borders)?;
|
||||
window.focus()?;
|
||||
|
||||
Ok(())
|
||||
@@ -865,6 +1040,7 @@ impl WindowManager {
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
let monitor = self
|
||||
@@ -884,7 +1060,7 @@ impl WindowManager {
|
||||
|
||||
// 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)?;
|
||||
workspace.update(&work_area, &invisible_borders)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
@@ -1014,6 +1190,18 @@ impl WindowManager {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn monitor_idx_from_current_pos(&mut self) -> Option<usize> {
|
||||
let hmonitor = WindowsApi::monitor_from_point(WindowsApi::cursor_pos().ok()?);
|
||||
|
||||
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()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
@@ -1068,6 +1256,12 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no container"))
|
||||
}
|
||||
|
||||
pub fn focused_window(&self) -> Result<&Window> {
|
||||
self.focused_container()?
|
||||
.focused_window()
|
||||
.ok_or_else(|| anyhow!("there is no window"))
|
||||
}
|
||||
|
||||
fn focused_window_mut(&mut self) -> Result<&mut Window> {
|
||||
self.focused_container_mut()?
|
||||
.focused_window_mut()
|
||||
|
||||
@@ -16,6 +16,8 @@ pub enum WindowManagerEvent {
|
||||
MouseCapture(WinEvent, Window),
|
||||
Manage(Window),
|
||||
Unmanage(Window),
|
||||
Raise(Window),
|
||||
MonitorPoll(WinEvent, Window),
|
||||
}
|
||||
|
||||
impl Display for WindowManagerEvent {
|
||||
@@ -60,6 +62,16 @@ impl Display for WindowManagerEvent {
|
||||
winevent, window
|
||||
)
|
||||
}
|
||||
WindowManagerEvent::Raise(window) => {
|
||||
write!(f, "Raise (Window: {})", window)
|
||||
}
|
||||
WindowManagerEvent::MonitorPoll(winevent, window) => {
|
||||
write!(
|
||||
f,
|
||||
"MonitorPoll (WinEvent: {}, Window: {})",
|
||||
winevent, window
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +86,8 @@ impl WindowManagerEvent {
|
||||
| WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::MoveResizeEnd(_, window)
|
||||
| WindowManagerEvent::MouseCapture(_, window)
|
||||
| WindowManagerEvent::MonitorPoll(_, window)
|
||||
| WindowManagerEvent::Raise(window)
|
||||
| WindowManagerEvent::Manage(window)
|
||||
| WindowManagerEvent::Unmanage(window) => window,
|
||||
}
|
||||
@@ -116,6 +130,17 @@ impl WindowManagerEvent {
|
||||
None
|
||||
}
|
||||
}
|
||||
WinEvent::ObjectCreate => {
|
||||
if let Ok(title) = window.title() {
|
||||
// Hidden COM support mechanism window that fires this event on both DPI/scaling
|
||||
// changes and resolution changes, a good candidate for polling
|
||||
if title == "OLEChannelWnd" {
|
||||
return Option::from(Self::MonitorPoll(winevent, window));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::Error;
|
||||
use color_eyre::Result;
|
||||
|
||||
use bindings::Handle;
|
||||
use bindings::Result as WindowsCrateResult;
|
||||
use bindings::Windows::Win32::Foundation::BOOL;
|
||||
use bindings::Windows::Win32::Foundation::HANDLE;
|
||||
use bindings::Windows::Win32::Foundation::HWND;
|
||||
@@ -23,6 +25,7 @@ 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::GetMonitorInfoW;
|
||||
use bindings::Windows::Win32::Graphics::Gdi::MonitorFromPoint;
|
||||
use bindings::Windows::Win32::Graphics::Gdi::MonitorFromWindow;
|
||||
use bindings::Windows::Win32::Graphics::Gdi::HDC;
|
||||
use bindings::Windows::Win32::Graphics::Gdi::HMONITOR;
|
||||
@@ -59,6 +62,7 @@ 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::SystemParametersInfoW;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
||||
@@ -84,43 +88,12 @@ use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::set_window_position::SetWindowPosition;
|
||||
use crate::windows_callbacks;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
pub enum WindowsResult<T, E> {
|
||||
Err(E),
|
||||
Ok(T),
|
||||
}
|
||||
|
||||
impl From<BOOL> 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<HWND> for WindowsResult<isize, Error> {
|
||||
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<HANDLE> for WindowsResult<HANDLE, Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_integer_for_windows_result {
|
||||
( $( $integer_type:ty ),+ ) => {
|
||||
$(
|
||||
@@ -147,6 +120,40 @@ impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ProcessWindowsCrateResult<T> {
|
||||
fn process(self) -> Result<T>;
|
||||
}
|
||||
|
||||
macro_rules! impl_process_windows_crate_result {
|
||||
( $($input:ty => $deref:ty),+ $(,)? ) => (
|
||||
paste::paste! {
|
||||
$(
|
||||
impl ProcessWindowsCrateResult<$deref> for WindowsCrateResult<$input> {
|
||||
fn process(self) -> Result<$deref> {
|
||||
match self {
|
||||
Ok(value) => Ok(value.0),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
impl_process_windows_crate_result!(
|
||||
HWND => isize,
|
||||
);
|
||||
|
||||
impl<T> ProcessWindowsCrateResult<T> for WindowsCrateResult<T> {
|
||||
fn process(self) -> Result<T> {
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowsApi;
|
||||
|
||||
impl WindowsApi {
|
||||
@@ -154,14 +161,27 @@ impl WindowsApi {
|
||||
callback: MONITORENUMPROC,
|
||||
callback_data_address: isize,
|
||||
) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
unsafe {
|
||||
EnumDisplayMonitors(
|
||||
HDC(0),
|
||||
std::ptr::null_mut(),
|
||||
Option::from(callback),
|
||||
LPARAM(callback_data_address),
|
||||
)
|
||||
}))
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn valid_hmonitors() -> Result<Vec<isize>> {
|
||||
let mut monitors: Vec<isize> = vec![];
|
||||
let monitors_ref: &mut Vec<isize> = monitors.as_mut();
|
||||
Self::enum_display_monitors(
|
||||
windows_callbacks::valid_display_monitors,
|
||||
monitors_ref as *mut Vec<isize> as isize,
|
||||
)?;
|
||||
|
||||
Ok(monitors)
|
||||
}
|
||||
|
||||
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||
@@ -172,16 +192,15 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
EnumWindows(Option::from(callback), LPARAM(callback_data_address))
|
||||
}))
|
||||
unsafe { EnumWindows(Option::from(callback), LPARAM(callback_data_address)) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||
for monitor in monitors.elements_mut() {
|
||||
if monitor.workspaces().is_empty() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
let monitor_id = monitor.id();
|
||||
if let Some(workspace) = monitor.workspaces_mut().front_mut() {
|
||||
// EnumWindows will enumerate through windows on all monitors
|
||||
Self::enum_windows(
|
||||
windows_callbacks::enum_window,
|
||||
@@ -198,7 +217,7 @@ impl WindowsApi {
|
||||
|
||||
for container in workspace.containers_mut() {
|
||||
for window in container.windows() {
|
||||
if Self::monitor_from_window(window.hwnd()) != monitor.id() {
|
||||
if Self::monitor_from_window(window.hwnd()) != monitor_id {
|
||||
windows_on_other_monitors.push(window.hwnd().0);
|
||||
}
|
||||
}
|
||||
@@ -207,8 +226,6 @@ impl WindowsApi {
|
||||
for hwnd in windows_on_other_monitors {
|
||||
workspace.remove_window(hwnd)?;
|
||||
}
|
||||
|
||||
monitor.workspaces_mut().push_back(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,9 +233,9 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
AllowSetForegroundWindow(process_id)
|
||||
}))
|
||||
unsafe { AllowSetForegroundWindow(process_id) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn monitor_from_window(hwnd: HWND) -> isize {
|
||||
@@ -227,6 +244,12 @@ impl WindowsApi {
|
||||
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
|
||||
}
|
||||
|
||||
pub fn monitor_from_point(point: POINT) -> 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 { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
|
||||
}
|
||||
|
||||
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_ACTIVATE;
|
||||
|
||||
@@ -235,7 +258,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
position,
|
||||
@@ -245,7 +268,9 @@ impl WindowsApi {
|
||||
layout.bottom,
|
||||
SET_WINDOW_POS_FLAGS(flags),
|
||||
)
|
||||
}))
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||
@@ -267,39 +292,25 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn foreground_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetForegroundWindow() }))
|
||||
unsafe { GetForegroundWindow() }.ok().process()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { SetForegroundWindow(hwnd) }.ok().process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn top_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetTopWindow(HWND::NULL).0 }))
|
||||
unsafe { GetTopWindow(HWND::default()) }.ok().process()
|
||||
}
|
||||
|
||||
pub fn desktop_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetDesktopWindow() }))
|
||||
unsafe { GetDesktopWindow() }.ok().process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn next_window(hwnd: HWND) -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindow(hwnd, GW_HWNDNEXT).0
|
||||
}))
|
||||
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok().process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -320,28 +331,30 @@ impl WindowsApi {
|
||||
|
||||
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
||||
let mut rect = unsafe { std::mem::zeroed() };
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowRect(hwnd, &mut rect)
|
||||
}))?;
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.ok().process()?;
|
||||
|
||||
Ok(Rect::from(rect))
|
||||
}
|
||||
|
||||
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe { SetCursorPos(x, y) }))
|
||||
unsafe { SetCursorPos(x, y) }.ok().process()
|
||||
}
|
||||
|
||||
pub fn cursor_pos() -> Result<POINT> {
|
||||
let mut cursor_pos: POINT = unsafe { std::mem::zeroed() };
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetCursorPos(&mut cursor_pos)
|
||||
}))?;
|
||||
let mut cursor_pos = POINT::default();
|
||||
unsafe { GetCursorPos(&mut cursor_pos) }.ok().process()?;
|
||||
|
||||
Ok(cursor_pos)
|
||||
}
|
||||
|
||||
pub fn window_from_point(point: POINT) -> Result<isize> {
|
||||
unsafe { WindowFromPoint(point) }.ok().process()
|
||||
}
|
||||
|
||||
pub fn window_at_cursor_pos() -> Result<isize> {
|
||||
Self::window_from_point(Self::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))
|
||||
}
|
||||
@@ -365,24 +378,13 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
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)
|
||||
}))
|
||||
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { SetFocus(hwnd) }.ok().map(|_| ()).process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -434,9 +436,9 @@ impl WindowsApi {
|
||||
inherit_handle: bool,
|
||||
process_id: u32,
|
||||
) -> Result<HANDLE> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
OpenProcess(access_rights, inherit_handle, process_id)
|
||||
}))
|
||||
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
|
||||
@@ -448,14 +450,16 @@ impl WindowsApi {
|
||||
let mut path: Vec<u16> = vec![0; len as usize];
|
||||
let text_ptr = path.as_mut_ptr();
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
unsafe {
|
||||
QueryFullProcessImageNameW(
|
||||
handle,
|
||||
PROCESS_NAME_FORMAT(0),
|
||||
PWSTR(text_ptr),
|
||||
&mut len as *mut u32,
|
||||
)
|
||||
}))?;
|
||||
}
|
||||
.ok()
|
||||
.process()?;
|
||||
|
||||
Ok(String::from_utf16(&path[..len as usize])?)
|
||||
}
|
||||
@@ -530,34 +534,36 @@ impl WindowsApi {
|
||||
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
|
||||
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast())
|
||||
}))?;
|
||||
unsafe { GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast()) }
|
||||
.ok()
|
||||
.process()?;
|
||||
|
||||
Ok(monitor_info)
|
||||
}
|
||||
|
||||
pub fn monitor(hmonitor: HMONITOR) -> Result<Monitor> {
|
||||
let monitor_info = Self::monitor_info_w(hmonitor)?;
|
||||
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
|
||||
let monitor_info = Self::monitor_info_w(HMONITOR(hmonitor))?;
|
||||
|
||||
Ok(monitor::new(
|
||||
hmonitor.0,
|
||||
hmonitor,
|
||||
monitor_info.rcMonitor.into(),
|
||||
monitor_info.rcWork.into(),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn system_parameters_info_w(
|
||||
action: SYSTEM_PARAMETERS_INFO_ACTION,
|
||||
ui_param: u32,
|
||||
pv_param: *mut c_void,
|
||||
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
|
||||
) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
SystemParametersInfoW(action, ui_param, pv_param, update_flags)
|
||||
}))
|
||||
unsafe { SystemParametersInfoW(action, ui_param, pv_param, update_flags) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn focus_follows_mouse() -> Result<bool> {
|
||||
let mut is_enabled: BOOL = unsafe { std::mem::zeroed() };
|
||||
|
||||
@@ -571,6 +577,7 @@ impl WindowsApi {
|
||||
Ok(is_enabled.into())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_focus_follows_mouse() -> Result<()> {
|
||||
Self::system_parameters_info_w(
|
||||
SPI_SETACTIVEWINDOWTRACKING,
|
||||
@@ -580,6 +587,7 @@ impl WindowsApi {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn disable_focus_follows_mouse() -> Result<()> {
|
||||
Self::system_parameters_info_w(
|
||||
SPI_SETACTIVEWINDOWTRACKING,
|
||||
|
||||
@@ -16,6 +16,17 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||
|
||||
pub extern "system" fn valid_display_monitors(
|
||||
hmonitor: HMONITOR,
|
||||
_: HDC,
|
||||
_: *mut RECT,
|
||||
lparam: LPARAM,
|
||||
) -> BOOL {
|
||||
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
monitors.push(hmonitor.0);
|
||||
true.into()
|
||||
}
|
||||
|
||||
pub extern "system" fn enum_display_monitor(
|
||||
hmonitor: HMONITOR,
|
||||
_: HDC,
|
||||
@@ -23,7 +34,15 @@ pub extern "system" fn enum_display_monitor(
|
||||
lparam: LPARAM,
|
||||
) -> BOOL {
|
||||
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
|
||||
if let Ok(m) = WindowsApi::monitor(hmonitor) {
|
||||
|
||||
// Don't duplicate a monitor that is already being managed
|
||||
for monitor in monitors.elements() {
|
||||
if monitor.id() == hmonitor.0 {
|
||||
return true.into();
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(m) = WindowsApi::monitor(hmonitor.0) {
|
||||
monitors.elements_mut().push_back(m);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::collections::VecDeque;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
@@ -48,8 +47,7 @@ pub struct Workspace {
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
latest_layout: Vec<Rect>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
resize_dimensions: Vec<Option<Rect>>,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
tile: bool,
|
||||
@@ -139,7 +137,7 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(&mut self, work_area: &Rect) -> Result<()> {
|
||||
pub fn update(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
|
||||
let mut adjusted_work_area = *work_area;
|
||||
adjusted_work_area.add_padding(self.workspace_padding());
|
||||
|
||||
@@ -148,16 +146,18 @@ impl Workspace {
|
||||
if *self.tile() {
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
if let Some(window) = container.focused_window_mut() {
|
||||
window.set_position(&adjusted_work_area, true)?;
|
||||
window.set_position(&adjusted_work_area, invisible_borders, true)?;
|
||||
};
|
||||
} else if let Some(window) = self.maximized_window_mut() {
|
||||
window.maximize();
|
||||
} else if !self.containers().is_empty() {
|
||||
let layouts = self.layout().calculate(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).context(
|
||||
"there must be at least one container to calculate a workspace layout",
|
||||
)?,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"there must be at least one container to calculate a workspace layout"
|
||||
)
|
||||
})?,
|
||||
self.container_padding(),
|
||||
self.layout_flip(),
|
||||
self.resize_dimensions(),
|
||||
@@ -166,7 +166,7 @@ impl Workspace {
|
||||
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, false)?;
|
||||
window.set_position(layout, invisible_borders, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,38 @@ impl Workspace {
|
||||
idx
|
||||
}
|
||||
|
||||
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||
for container in self.containers() {
|
||||
if let Some(hwnd) = container.hwnd_from_exe(exe) {
|
||||
return Option::from(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container() {
|
||||
if let Some(hwnd) = container.hwnd_from_exe(exe) {
|
||||
return Option::from(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||
for container in self.containers() {
|
||||
if container.contains_window(hwnd) {
|
||||
@@ -292,11 +324,14 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn promote_container(&mut self) -> Result<()> {
|
||||
let resize = self.resize_dimensions_mut().remove(0);
|
||||
let container = self
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
self.containers_mut().push_front(container);
|
||||
self.resize_dimensions_mut().insert(0, None);
|
||||
self.resize_dimensions_mut().insert(0, resize);
|
||||
|
||||
self.focus_container(0);
|
||||
|
||||
Ok(())
|
||||
@@ -308,8 +343,15 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||
self.resize_dimensions_mut().remove(idx);
|
||||
self.containers_mut().remove(idx)
|
||||
if idx < self.resize_dimensions().len() {
|
||||
self.resize_dimensions_mut().remove(idx);
|
||||
}
|
||||
|
||||
if idx < self.containers().len() {
|
||||
return self.containers_mut().remove(idx);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
; Generated by komorebic.exe
|
||||
|
||||
Start() {
|
||||
Run, komorebic.exe start, , Hide
|
||||
Start(ffm) {
|
||||
Run, komorebic.exe start --ffm %ffm%, , Hide
|
||||
}
|
||||
|
||||
Stop() {
|
||||
@@ -12,10 +12,30 @@ State() {
|
||||
Run, komorebic.exe state, , Hide
|
||||
}
|
||||
|
||||
Query(state_query) {
|
||||
Run, komorebic.exe query %state_query%, , Hide
|
||||
}
|
||||
|
||||
Log() {
|
||||
Run, komorebic.exe log, , Hide
|
||||
}
|
||||
|
||||
QuickSave() {
|
||||
Run, komorebic.exe quick-save, , Hide
|
||||
}
|
||||
|
||||
QuickLoad() {
|
||||
Run, komorebic.exe quick-load, , Hide
|
||||
}
|
||||
|
||||
Save(path) {
|
||||
Run, komorebic.exe save %path%, , Hide
|
||||
}
|
||||
|
||||
Load(path) {
|
||||
Run, komorebic.exe load %path%, , Hide
|
||||
}
|
||||
|
||||
Focus(operation_direction) {
|
||||
Run, komorebic.exe focus %operation_direction%, , Hide
|
||||
}
|
||||
@@ -68,6 +88,10 @@ NewWorkspace() {
|
||||
Run, komorebic.exe new-workspace, , Hide
|
||||
}
|
||||
|
||||
InvisibleBorders(left, top, right, bottom) {
|
||||
Run, komorebic.exe invisible-borders %left% %top% %right% %bottom%, , Hide
|
||||
}
|
||||
|
||||
AdjustContainerPadding(sizing, adjustment) {
|
||||
Run, komorebic.exe adjust-container-padding %sizing% %adjustment%, , Hide
|
||||
}
|
||||
@@ -172,12 +196,16 @@ IdentifyTrayApplication(identifier, id) {
|
||||
Run, komorebic.exe identify-tray-application %identifier% %id%, , Hide
|
||||
}
|
||||
|
||||
FocusFollowsMouse(boolean_state) {
|
||||
Run, komorebic.exe focus-follows-mouse %boolean_state%, , Hide
|
||||
IdentifyBorderOverflow(identifier, id) {
|
||||
Run, komorebic.exe identify-border-overflow %identifier% %id%, , Hide
|
||||
}
|
||||
|
||||
ToggleFocusFollowsMouse() {
|
||||
Run, komorebic.exe toggle-focus-follows-mouse, , Hide
|
||||
FocusFollowsMouse(boolean_state, implementation) {
|
||||
Run, komorebic.exe focus-follows-mouse %boolean_state% --implementation %implementation%, , Hide
|
||||
}
|
||||
|
||||
ToggleFocusFollowsMouse(implementation) {
|
||||
Run, komorebic.exe toggle-focus-follows-mouse --implementation %implementation%, , Hide
|
||||
}
|
||||
|
||||
AhkLibrary() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
|
||||
@@ -9,12 +9,11 @@ use std::io::ErrorKind;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::stringify;
|
||||
|
||||
use clap::AppSettings;
|
||||
use clap::ArgEnum;
|
||||
use clap::Clap;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use fs_tail::TailedFile;
|
||||
use heck::KebabCase;
|
||||
@@ -31,10 +30,13 @@ use derive_ahk::AhkLibrary;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::StateQuery;
|
||||
|
||||
trait AhkLibrary {
|
||||
fn generate_ahk_library() -> String;
|
||||
@@ -61,7 +63,7 @@ impl From<BooleanState> for bool {
|
||||
|
||||
macro_rules! gen_enum_subcommand_args {
|
||||
// SubCommand Pattern: Enum Type
|
||||
( $( $name:ident: $element:ty ),+ ) => {
|
||||
( $( $name:ident: $element:ty ),+ $(,)? ) => {
|
||||
$(
|
||||
paste! {
|
||||
#[derive(clap::Clap, derive_ahk::AhkFunction)]
|
||||
@@ -82,12 +84,12 @@ gen_enum_subcommand_args! {
|
||||
FlipLayout: Flip,
|
||||
ChangeLayout: Layout,
|
||||
WatchConfiguration: BooleanState,
|
||||
FocusFollowsMouse: BooleanState
|
||||
Query: StateQuery,
|
||||
}
|
||||
|
||||
macro_rules! gen_target_subcommand_args {
|
||||
// SubCommand Pattern
|
||||
( $( $name:ident ),+ ) => {
|
||||
( $( $name:ident ),+ $(,)? ) => {
|
||||
$(
|
||||
#[derive(clap::Clap, derive_ahk::AhkFunction)]
|
||||
pub struct $name {
|
||||
@@ -104,7 +106,7 @@ gen_target_subcommand_args! {
|
||||
SendToMonitor,
|
||||
SendToWorkspace,
|
||||
FocusMonitor,
|
||||
FocusWorkspace
|
||||
FocusWorkspace,
|
||||
}
|
||||
|
||||
// Thanks to @danielhenrymantilla for showing me how to use cfg_attr with an optional argument like
|
||||
@@ -112,7 +114,7 @@ gen_target_subcommand_args! {
|
||||
macro_rules! gen_workspace_subcommand_args {
|
||||
// Workspace Property: #[enum] Value Enum (if the value is an Enum)
|
||||
// Workspace Property: Value Type (if the value is anything else)
|
||||
( $( $name:ident: $(#[enum] $(@$arg_enum:tt)?)? $value:ty ),+ ) => (
|
||||
( $( $name:ident: $(#[enum] $(@$arg_enum:tt)?)? $value:ty ),+ $(,)? ) => (
|
||||
paste! {
|
||||
$(
|
||||
#[derive(clap::Clap, derive_ahk::AhkFunction)]
|
||||
@@ -138,7 +140,7 @@ macro_rules! gen_workspace_subcommand_args {
|
||||
gen_workspace_subcommand_args! {
|
||||
Name: String,
|
||||
Layout: #[enum] Layout,
|
||||
Tiling: #[enum] BooleanState
|
||||
Tiling: #[enum] BooleanState,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
@@ -149,6 +151,18 @@ struct Resize {
|
||||
sizing: Sizing,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct InvisibleBorders {
|
||||
/// Size of the left invisible border
|
||||
left: i32,
|
||||
/// Size of the top invisible border (usually 0)
|
||||
top: i32,
|
||||
/// Size of the right invisible border (usually left * 2)
|
||||
right: i32,
|
||||
/// Size of the bottom invisible border (usually the same as left)
|
||||
bottom: i32,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct EnsureWorkspaces {
|
||||
/// Monitor index (zero-indexed)
|
||||
@@ -159,7 +173,7 @@ struct EnsureWorkspaces {
|
||||
|
||||
macro_rules! gen_padding_subcommand_args {
|
||||
// SubCommand Pattern
|
||||
( $( $name:ident ),+ ) => {
|
||||
( $( $name:ident ),+ $(,)? ) => {
|
||||
$(
|
||||
#[derive(clap::Clap, derive_ahk::AhkFunction)]
|
||||
pub struct $name {
|
||||
@@ -176,12 +190,12 @@ macro_rules! gen_padding_subcommand_args {
|
||||
|
||||
gen_padding_subcommand_args! {
|
||||
ContainerPadding,
|
||||
WorkspacePadding
|
||||
WorkspacePadding,
|
||||
}
|
||||
|
||||
macro_rules! gen_padding_adjustment_subcommand_args {
|
||||
// SubCommand Pattern
|
||||
( $( $name:ident ),+ ) => {
|
||||
( $( $name:ident ),+ $(,)? ) => {
|
||||
$(
|
||||
#[derive(clap::Clap, derive_ahk::AhkFunction)]
|
||||
pub struct $name {
|
||||
@@ -196,12 +210,12 @@ macro_rules! gen_padding_adjustment_subcommand_args {
|
||||
|
||||
gen_padding_adjustment_subcommand_args! {
|
||||
AdjustContainerPadding,
|
||||
AdjustWorkspacePadding
|
||||
AdjustWorkspacePadding,
|
||||
}
|
||||
|
||||
macro_rules! gen_application_target_subcommand_args {
|
||||
// SubCommand Pattern
|
||||
( $( $name:ident ),+ ) => {
|
||||
( $( $name:ident ),+ $(,)? ) => {
|
||||
$(
|
||||
#[derive(clap::Clap, derive_ahk::AhkFunction)]
|
||||
pub struct $name {
|
||||
@@ -217,7 +231,8 @@ macro_rules! gen_application_target_subcommand_args {
|
||||
gen_application_target_subcommand_args! {
|
||||
FloatRule,
|
||||
ManageRule,
|
||||
IdentifyTrayApplication
|
||||
IdentifyTrayApplication,
|
||||
IdentifyBorderOverflow,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
@@ -232,6 +247,39 @@ struct WorkspaceRule {
|
||||
workspace: usize,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct ToggleFocusFollowsMouse {
|
||||
#[clap(arg_enum, short, long, default_value = "windows")]
|
||||
implementation: FocusFollowsMouseImplementation,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct FocusFollowsMouse {
|
||||
#[clap(arg_enum, short, long, default_value = "windows")]
|
||||
implementation: FocusFollowsMouseImplementation,
|
||||
#[clap(arg_enum)]
|
||||
boolean_state: BooleanState,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Start {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(long)]
|
||||
ffm: bool,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Save {
|
||||
/// File to which the resize layout dimensions should be saved
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Load {
|
||||
/// File from which the resize layout dimensions should be loaded
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
|
||||
struct Opts {
|
||||
@@ -242,13 +290,26 @@ struct Opts {
|
||||
#[derive(Clap, AhkLibrary)]
|
||||
enum SubCommand {
|
||||
/// Start komorebi.exe as a background process
|
||||
Start,
|
||||
Start(Start),
|
||||
/// Stop the komorebi.exe process and restore all hidden windows
|
||||
Stop,
|
||||
/// Show a JSON representation of the current window manager state
|
||||
State,
|
||||
/// Query the current window manager state
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Query(Query),
|
||||
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
Log,
|
||||
/// Quicksave the current resize layout dimensions
|
||||
QuickSave,
|
||||
/// Load the last quicksaved resize layout dimensions
|
||||
QuickLoad,
|
||||
/// Save the current resize layout dimensions to a file
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Save(Save),
|
||||
/// Load the resize layout dimensions from a file
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Load(Load),
|
||||
/// Change focus to the window in the specified direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Focus(Focus),
|
||||
@@ -286,6 +347,9 @@ enum SubCommand {
|
||||
FocusWorkspace(FocusWorkspace),
|
||||
/// Create and append a new workspace on the focused monitor
|
||||
NewWorkspace,
|
||||
/// Set the invisible border dimensions around each window
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
InvisibleBorders(InvisibleBorders),
|
||||
/// Adjust container padding on the focused workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
AdjustContainerPadding(AdjustContainerPadding),
|
||||
@@ -353,17 +417,21 @@ enum SubCommand {
|
||||
/// Identify an application that closes to the system tray
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
IdentifyTrayApplication(IdentifyTrayApplication),
|
||||
/// Identify an application that has overflowing borders
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
IdentifyBorderOverflow(IdentifyBorderOverflow),
|
||||
/// Enable or disable focus follows mouse for the operating system
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
FocusFollowsMouse(FocusFollowsMouse),
|
||||
/// Toggle focus follows mouse for the operating system
|
||||
ToggleFocusFollowsMouse,
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
ToggleFocusFollowsMouse(ToggleFocusFollowsMouse),
|
||||
/// Generate a library of AutoHotKey helper functions
|
||||
AhkLibrary,
|
||||
}
|
||||
|
||||
pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
let mut socket = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut socket = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
socket.push("komorebi.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
@@ -377,7 +445,8 @@ fn main() -> Result<()> {
|
||||
|
||||
match opts.subcmd {
|
||||
SubCommand::AhkLibrary => {
|
||||
let mut library = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut library =
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
library.push("komorebic.lib.ahk");
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -389,9 +458,9 @@ fn main() -> Result<()> {
|
||||
|
||||
println!(
|
||||
"\nAHK helper library for komorebic written to {}",
|
||||
library
|
||||
.to_str()
|
||||
.context("could not find the path to the generated ahk lib file")?
|
||||
library.to_str().ok_or_else(|| anyhow!(
|
||||
"could not find the path to the generated ahk lib file"
|
||||
))?
|
||||
);
|
||||
|
||||
println!(
|
||||
@@ -436,6 +505,17 @@ fn main() -> Result<()> {
|
||||
SubCommand::SendToWorkspace(arg) => {
|
||||
send_message(&*SocketMessage::SendContainerToWorkspaceNumber(arg.target).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::InvisibleBorders(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::InvisibleBorders(Rect {
|
||||
left: arg.left,
|
||||
top: arg.top,
|
||||
right: arg.right,
|
||||
bottom: arg.bottom,
|
||||
})
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::ContainerPadding(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::ContainerPadding(arg.monitor, arg.workspace, arg.size)
|
||||
@@ -458,8 +538,8 @@ fn main() -> Result<()> {
|
||||
&*SocketMessage::AdjustContainerPadding(arg.sizing, arg.adjustment).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::ToggleFocusFollowsMouse => {
|
||||
send_message(&*SocketMessage::ToggleFocusFollowsMouse.as_bytes()?)?;
|
||||
SubCommand::ToggleFocusFollowsMouse(arg) => {
|
||||
send_message(&*SocketMessage::ToggleFocusFollowsMouse(arg.implementation).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ToggleTiling => {
|
||||
send_message(&*SocketMessage::ToggleTiling.as_bytes()?)?;
|
||||
@@ -485,7 +565,7 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::Start => {
|
||||
SubCommand::Start(arg) => {
|
||||
let mut buf: PathBuf;
|
||||
|
||||
// The komorebi.ps1 shim will only exist in the Path if installed by Scoop
|
||||
@@ -498,21 +578,36 @@ fn main() -> Result<()> {
|
||||
buf.pop(); // %USERPROFILE%\scoop\shims
|
||||
buf.pop(); // %USERPROFILE%\scoop
|
||||
buf.push("apps\\komorebi\\current\\komorebi.exe"); //%USERPROFILE%\scoop\komorebi\current\komorebi.exe
|
||||
Option::from(
|
||||
buf.to_str()
|
||||
.context("cannot create a string from the scoop komorebi path")?,
|
||||
)
|
||||
Option::from(buf.to_str().ok_or_else(|| {
|
||||
anyhow!("cannot create a string from the scoop komorebi path")
|
||||
})?)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let script = if let Some(exec) = exec {
|
||||
format!("Start-Process '{}' -WindowStyle hidden", exec)
|
||||
} else {
|
||||
String::from("Start-Process komorebi -WindowStyle hidden")
|
||||
};
|
||||
let script = exec.map_or_else(
|
||||
|| {
|
||||
if arg.ffm {
|
||||
String::from(
|
||||
"Start-Process komorebi.exe -ArgumentList '--ffm' -WindowStyle hidden",
|
||||
)
|
||||
} else {
|
||||
String::from("Start-Process komorebi.exe -WindowStyle hidden")
|
||||
}
|
||||
},
|
||||
|exec| {
|
||||
if arg.ffm {
|
||||
format!(
|
||||
"Start-Process '{}' -ArgumentList '--ffm' -WindowStyle hidden",
|
||||
exec
|
||||
)
|
||||
} else {
|
||||
format!("Start-Process '{}' -WindowStyle hidden", exec)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match powershell_script::run(&script, true) {
|
||||
Ok(output) => {
|
||||
@@ -575,7 +670,7 @@ fn main() -> Result<()> {
|
||||
)?;
|
||||
}
|
||||
SubCommand::State => {
|
||||
let home = dirs::home_dir().context("there is no home directory")?;
|
||||
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
@@ -608,8 +703,43 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::Query(arg) => {
|
||||
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.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());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
send_message(&*SocketMessage::Query(arg.state_query).as_bytes()?)?;
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::RestoreWindows => {
|
||||
let mut hwnd_json = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut hwnd_json =
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
hwnd_json.push("komorebi.hwnd.json");
|
||||
|
||||
let file = File::open(hwnd_json)?;
|
||||
@@ -629,7 +759,9 @@ fn main() -> Result<()> {
|
||||
BooleanState::Disable => false,
|
||||
};
|
||||
|
||||
send_message(&*SocketMessage::FocusFollowsMouse(enable).as_bytes()?)?;
|
||||
send_message(
|
||||
&*SocketMessage::FocusFollowsMouse(arg.implementation, enable).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::ReloadConfiguration => {
|
||||
send_message(&*SocketMessage::ReloadConfiguration.as_bytes()?)?;
|
||||
@@ -647,17 +779,65 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::IdentifyBorderOverflow(target) => {
|
||||
send_message(
|
||||
&*SocketMessage::IdentifyBorderOverflow(target.identifier, target.id).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::Manage => {
|
||||
send_message(&*SocketMessage::ManageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Unmanage => {
|
||||
send_message(&*SocketMessage::UnmanageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::QuickSave => {
|
||||
send_message(&*SocketMessage::QuickSave.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::QuickLoad => {
|
||||
send_message(&*SocketMessage::QuickLoad.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Save(arg) => {
|
||||
send_message(&*SocketMessage::Save(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Load(arg) => {
|
||||
send_message(&*SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_windows_path(raw_path: &str) -> Result<PathBuf> {
|
||||
let path = if raw_path.starts_with('~') {
|
||||
raw_path.replacen(
|
||||
"~",
|
||||
&dirs::home_dir()
|
||||
.ok_or_else(|| anyhow!("there is no home directory"))?
|
||||
.display()
|
||||
.to_string(),
|
||||
1,
|
||||
)
|
||||
} else {
|
||||
raw_path.to_string()
|
||||
};
|
||||
|
||||
let full_path = PathBuf::from(path);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot parse directory"))?;
|
||||
|
||||
let file = full_path
|
||||
.components()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("cannot parse filename"))?;
|
||||
|
||||
let mut canonicalized = std::fs::canonicalize(parent)?;
|
||||
canonicalized.push(file);
|
||||
|
||||
Ok(canonicalized)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user