mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-15 17:57:40 +01:00
Compare commits
25 Commits
v0.1.5
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
987dc2b8dd | ||
|
|
29a6c39084 | ||
|
|
5d0806a8c9 | ||
|
|
6c53fd7830 | ||
|
|
6ae59671a2 | ||
|
|
f17bfe267e | ||
|
|
840af215a0 | ||
|
|
6981d778a9 | ||
|
|
5d6351f48d | ||
|
|
ac0f33f7ed | ||
|
|
f19bd3032b | ||
|
|
3f3c2815da | ||
|
|
7070878f4a | ||
|
|
d3cb9e07f7 | ||
|
|
6f6181625f | ||
|
|
80dd07fcde | ||
|
|
09d1d69668 | ||
|
|
786f5e846a | ||
|
|
65bc1a966e | ||
|
|
ddafe599a2 | ||
|
|
7ed6df511f | ||
|
|
f9c4dbd447 | ||
|
|
b344888b72 | ||
|
|
a62ed682de | ||
|
|
94e9bb8e9e |
155
Cargo.lock
generated
155
Cargo.lock
generated
@@ -73,9 +73,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.70"
|
||||
version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
|
||||
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -199,9 +199,9 @@ checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
@@ -249,9 +249,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.2.0"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1"
|
||||
checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"winapi 0.3.9",
|
||||
@@ -268,9 +268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "3.0.2"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
@@ -286,6 +286,12 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
@@ -425,9 +431,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hotwatch"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d61ee702e77f237b41761361a82e5c4bf6277dbb4bc8b6b7d745cb249cc82b31"
|
||||
checksum = "39301670a6f5798b75f36a1b149a379a50df5aa7c71be50f4b41ec6eab445cb8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify",
|
||||
@@ -471,9 +477,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.10"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
|
||||
checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
@@ -505,7 +511,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebi"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"bitflags",
|
||||
@@ -519,6 +525,7 @@ dependencies = [
|
||||
"hotwatch",
|
||||
"komorebi-core",
|
||||
"lazy_static",
|
||||
"miow 0.3.7",
|
||||
"nanoid",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
@@ -537,19 +544,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "komorebic"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
@@ -580,9 +588,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.102"
|
||||
version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
|
||||
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -649,7 +663,7 @@ dependencies = [
|
||||
"kernel32-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"miow",
|
||||
"miow 0.2.2",
|
||||
"net2",
|
||||
"slab",
|
||||
"winapi 0.2.8",
|
||||
@@ -679,6 +693,15 @@ dependencies = [
|
||||
"ws2_32-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
@@ -701,9 +724,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187"
|
||||
checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cc",
|
||||
@@ -853,9 +876,9 @@ checksum = "36d62894f5590e88d99d0d82918742ba8e5bff1985af15d4906b6a65f635adb2"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.10"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
@@ -883,18 +906,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.29"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||
checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
||||
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1112,25 +1135,37 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.3"
|
||||
name = "serde_yaml"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "740223c51853f3145fe7c90360d2d4232f2b62e3449489c207eccde818979982"
|
||||
checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
|
||||
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.6.1"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
@@ -1161,9 +1196,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.76"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
|
||||
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1172,9 +1207,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.20.3"
|
||||
version = "0.20.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d77883450d697c0010e60db3d940ed130b0ed81d27485edee981621b434e52"
|
||||
checksum = "e223c65cd36b485a34c2ce6e38efa40777d31c4166d9076030c74cdcf971679f"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"core-foundation-sys",
|
||||
@@ -1245,9 +1280,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.28"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
|
||||
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"pin-project-lite",
|
||||
@@ -1268,9 +1303,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.16"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98863d0dd09fa59a1b79c6750ad80dbda6b75f4e71c437a6a1a8cb91a8bcbd77"
|
||||
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1279,9 +1314,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf"
|
||||
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
@@ -1319,9 +1354,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.2.24"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
|
||||
checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"chrono",
|
||||
@@ -1452,20 +1487,21 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7524f6f9074f6326a1c167cd3dc2ed4e6916648a1a55116d029620af9b65fb1"
|
||||
checksum = "b8f5f8d2ea79bf690bbee453fd4a1516ae426e5d5c7215d96cc0c3dc134fc4a0"
|
||||
dependencies = [
|
||||
"const-sha1",
|
||||
"windows_gen",
|
||||
"windows_macros",
|
||||
"windows_reader",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_gen"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4be44a189bde96fc0e0cdd5b152b2d21c635c0c94c7d256aab4425477b2a2f37"
|
||||
checksum = "7e6994f42f8481387778cc608407d6703410672d57f32a66009419d7a18aa912"
|
||||
dependencies = [
|
||||
"windows_quote",
|
||||
"windows_reader",
|
||||
@@ -1473,9 +1509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_macros"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1d78ce8a43d45b8da282383a2cb2ffcd5587cc3a9c341125d3181d2b701ede"
|
||||
checksum = "81cc2357b1b03c19f056cb0e6d06011f80f54beadb4e36aee2ca98493c7cfc3c"
|
||||
dependencies = [
|
||||
"syn",
|
||||
"windows_gen",
|
||||
@@ -1485,15 +1521,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_quote"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51fa2185b18a6164a3fa3ea2b6c92ebc1b60f532ae5a85c57408ba6a5a064913"
|
||||
checksum = "7cf987b5288c15e1997226848f78f3ed3ef8b78dcfd71a201c8c8684163a7e4d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_reader"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3daa5bd758f2f8f20cd93a79aedca20759779f43785fc77b08a4e8e1e5876bbb"
|
||||
checksum = "237b53e8b40766ea7db5da0d8c6c1442d21d0429f0ee7500d7b5688967bd9d7b"
|
||||
|
||||
[[package]]
|
||||
name = "winput"
|
||||
@@ -1524,3 +1560,12 @@ dependencies = [
|
||||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
113
README.md
113
README.md
@@ -18,6 +18,10 @@ 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))
|
||||
|
||||
There is a [Discord server](https://discord.gg/vzBmPm6RkQ) available for _komorebi_-related discussion, help,
|
||||
troubleshooting etc. If you have any specific feature requests or bugs to report, please create an issue in this
|
||||
repository.
|
||||
|
||||
## Description
|
||||
|
||||
_komorebi_ only responds to [WinEvents](https://docs.microsoft.com/en-us/windows/win32/winauto/event-constants) and the
|
||||
@@ -204,6 +208,59 @@ dimensions and are not coupled to the applications that were running at the time
|
||||
When layouts that expect more or less windows than the number currently on the focused workspace are loaded, `komorebi`
|
||||
will automatically reconcile the difference.
|
||||
|
||||
#### Creating and Loading Custom Layouts
|
||||
|
||||
Particularly for users of ultrawide monitors, traditional tiling layouts may not seem like the most efficient use of
|
||||
screen space. If you feel this is the case with any of the default layouts, you are also welcome to create your own
|
||||
custom layouts and save them as JSON or YAML.
|
||||
|
||||
If you're not comfortable writing the layouts directly in JSON or YAML, you can use
|
||||
the [komorebi Custom Layout Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to interactively
|
||||
define a custom layout, and then copy the generated JSON content.
|
||||
|
||||
Custom layouts can be loaded on the current workspace or configured for a specific workspace with the following
|
||||
commands:
|
||||
|
||||
```powershell
|
||||
komorebic.exe load-custom-layout ~/custom.yaml
|
||||
komorebic.exe workspace-custom-layout 0 0 ~/custom.yaml
|
||||
```
|
||||
|
||||
The fundamental building block of a custom _komorebi_ layout is the Column.
|
||||
|
||||
Columns come in three variants:
|
||||
|
||||
- **Primary**: This is where your primary focus will be on the screen most of the time. There must be exactly one Primary
|
||||
Column in any custom layout. Optionally, you can specify the percentage of the screen width that you want the Primary
|
||||
Column to occupy.
|
||||
- **Secondary**: This is an optional column that can either be full height of split horizontally into a fixed number of
|
||||
maximum rows. There can be any number of Secondary Columns in a custom layout.
|
||||
- **Tertiary**: This is the final column where any remaining windows will be split horizontally into rows as they get added.
|
||||
|
||||
If there is only one window on the screen when a custom layout is selected, that window will take up the full work area
|
||||
of the screen.
|
||||
|
||||
If the number of windows is equal to or less than the total number of columns defined in a custom layout, the windows
|
||||
will be arranged in an equal-width columns.
|
||||
|
||||
When the number of windows is greater than the number of columns defined in the custom layout, the windows will begin to
|
||||
be arranged according to the constraints set on the Primary and Secondary columns of the layout.
|
||||
|
||||
Here is an example custom layout that can be used as a starting point for your own:
|
||||
|
||||
YAML
|
||||
|
||||
```yaml
|
||||
- column: Secondary
|
||||
configuration:
|
||||
Horizontal: 2 # max number of rows,
|
||||
- column: Primary
|
||||
configuration:
|
||||
WidthPercentage: 45 # percentage of screen
|
||||
- column: Tertiary
|
||||
configuration: Horizontal
|
||||
```
|
||||
|
||||
## Configuration with `komorebic`
|
||||
|
||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||
@@ -219,6 +276,8 @@ 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
|
||||
subscribe Subscribe to komorebi events
|
||||
unsubscribe Unsubscribe from komorebi events
|
||||
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
|
||||
@@ -226,6 +285,8 @@ save Save the current resize layout dimensions to a fil
|
||||
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
|
||||
cycle-focus Change focus to the window in the specified cycle direction
|
||||
cycle-move Move the focused window in the specified cycle direction
|
||||
stack Stack the focused window in the specified direction
|
||||
resize Resize the focused window in the specified direction
|
||||
unstack Unstack the focused window
|
||||
@@ -236,11 +297,15 @@ send-to-monitor Send the focused window to the specified monitor
|
||||
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
|
||||
cycle-monitor Focus the monitor in the given cycle direction
|
||||
cycle-workspace Focus the workspace in the given cycle direction
|
||||
new-workspace Create and append a new workspace on the focused monitor
|
||||
invisible-borders Set the invisible border dimensions around each window
|
||||
work-area-offset Set offsets to exclude parts of the work area from tiling
|
||||
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
|
||||
load-custom-layout Load a custom layout from file for the focused workspace
|
||||
flip-layout Flip the layout on the focused workspace (BSP only)
|
||||
promote Promote the focused window to the top of the tree
|
||||
retile Force the retiling of all managed windows
|
||||
@@ -248,6 +313,7 @@ ensure-workspaces Create at least this many workspaces for the speci
|
||||
container-padding Set the container padding for the specified workspace
|
||||
workspace-padding Set the workspace padding for the specified workspace
|
||||
workspace-layout Set the layout for the specified workspace
|
||||
workspace-custom-layout Set a custom layout for the specified workspace
|
||||
workspace-tiling Enable or disable window tiling for the specified workspace
|
||||
workspace-name Set the workspace name for the specified workspace
|
||||
toggle-pause Toggle the window manager on and off across all monitors
|
||||
@@ -301,14 +367,20 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Mouse drag to swap window container position
|
||||
- [x] Mouse drag to resize window container
|
||||
- [x] Configurable workspace and container gaps
|
||||
- [x] BSP tree layout
|
||||
- [x] BSP tree layout (`bsp`)
|
||||
- [x] Flip BSP tree layout horizontally or vertically
|
||||
- [x] Equal-width, max-height column layout
|
||||
- [x] Equal-width, max-height column layout (`columns`)
|
||||
- [x] Equal-height, max-width row layout (`rows`)
|
||||
- [x] Main half-height window with vertical stack layout (`horizontal-stack`)
|
||||
- [x] Main half-width window with horizontal stack layout (`vertical-stack`)
|
||||
- [x] 2x Main window (half and quarter-width) with horizontal stack layout (`ultrawide-vertical-stack`)
|
||||
- [x] Load custom layouts from JSON and YAML representations
|
||||
- [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 work area offsets to preserve space for custom taskbars
|
||||
- [x] Configure and compensate for the size of Windows 10's invisible borders
|
||||
- [x] Toggle floating windows
|
||||
- [x] Toggle monocle window
|
||||
@@ -323,6 +395,7 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Helper library for AutoHotKey
|
||||
- [x] View window manager state
|
||||
- [x] Query window manager state
|
||||
- [x] Subscribe to event and message notifications
|
||||
|
||||
## Development
|
||||
|
||||
@@ -380,3 +453,39 @@ representation of the `State` struct, which includes the current state of `Windo
|
||||
|
||||
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
|
||||
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
|
||||
|
||||
## Window Manager Event Subscriptions
|
||||
|
||||
It is also possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
by `komorebi` using [Named Pipes](https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes).
|
||||
|
||||
First, your application must create a named pipe. Once the named pipe has been created, run the following command:
|
||||
|
||||
```powershell
|
||||
komorebic.exe subscribe <your pipe name>
|
||||
```
|
||||
|
||||
Note that you do not have to include the full path of the named pipe, just the name.
|
||||
|
||||
If the named pipe exists, `komorebi` will start pushing JSON data of successfully handled events and messages:
|
||||
|
||||
```json lines
|
||||
{"event":{"type":"AddSubscriber","content":"yasb"},"state":{...}}
|
||||
{"event":{"type":"FocusWindow","content":"Left"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":131444,"title":"komorebi – README.md","exe":"idea64.exe","class":"SunAwtFrame","rect":{"left":13,"top":60,"right":1520,"bottom":1655}}]},"state":{...}}
|
||||
{"event":{"type":"MonitorPoll","content":["ObjectCreate",{"hwnd":5572450,"title":"OLEChannelWnd","exe":"explorer.exe","class":"OleMainThreadWndClass","rect":{"left":0,"top":0,"right":0,"bottom":0}}]},"state":{...}}
|
||||
{"event":{"type":"FocusWindow","content":"Right"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{}...}
|
||||
{"event":{"type":"FocusWindow","content":"Down"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":329264,"title":"den — Mozilla Firefox","exe":"firefox.exe","class":"MozillaWindowClass","rect":{"left":1539,"top":894,"right":1520,"bottom":821}}]},"state":{...}}
|
||||
{"event":{"type":"FocusWindow","content":"Up"},"state":{...}}
|
||||
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{...}}
|
||||
```
|
||||
|
||||
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
|
||||
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
|
||||
in `komorebi-core`.
|
||||
|
||||
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Python
|
||||
by [@denBot](https://github.com/denBot) can be
|
||||
found [here](https://gist.github.com/denBot/4136279812f87819f86d99eba77c1ee0).
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
name = "bindings"
|
||||
version = "0.1.0"
|
||||
authors = ["Jade Iqbal"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
windows = "0.20"
|
||||
windows = "0.21"
|
||||
|
||||
[build-dependencies]
|
||||
windows = "0.20"
|
||||
windows = "0.21"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-ahk"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.5"
|
||||
edition = "2018"
|
||||
version = "0.1.6"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -12,4 +12,5 @@ clap = "3.0.0-beta.4"
|
||||
color-eyre = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.8"
|
||||
strum = { version = "0.21", features = ["derive"] }
|
||||
|
||||
584
komorebi-core/src/arrangement.rs
Normal file
584
komorebi-core/src/arrangement.rs
Normal file
@@ -0,0 +1,584 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Rect;
|
||||
|
||||
pub trait Arrangement {
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect>;
|
||||
}
|
||||
|
||||
impl Arrangement for DefaultLayout {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
DefaultLayout::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
area,
|
||||
layout_flip,
|
||||
calculate_resize_adjustments(resize_dimensions),
|
||||
),
|
||||
DefaultLayout::Columns => columns(area, len),
|
||||
DefaultLayout::Rows => rows(area, len),
|
||||
DefaultLayout::VerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let primary_right = match len {
|
||||
1 => area.right,
|
||||
_ => area.right / 2,
|
||||
};
|
||||
|
||||
let mut main_left = area.left;
|
||||
let mut stack_left = area.left + primary_right;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
main_left = main_left + area.right - primary_right;
|
||||
stack_left = area.left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: main_left,
|
||||
top: area.top,
|
||||
right: primary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len > 1 {
|
||||
layouts.append(&mut rows(
|
||||
&Rect {
|
||||
left: stack_left,
|
||||
top: area.top,
|
||||
right: area.right - primary_right,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
len - 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
DefaultLayout::HorizontalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let bottom = match len {
|
||||
1 => area.bottom,
|
||||
_ => area.bottom / 2,
|
||||
};
|
||||
|
||||
let mut main_top = area.top;
|
||||
let mut stack_top = area.top + bottom;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Vertical | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
main_top = main_top + area.bottom - bottom;
|
||||
stack_top = area.top;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: area.left,
|
||||
top: main_top,
|
||||
right: area.right,
|
||||
bottom,
|
||||
});
|
||||
|
||||
if len > 1 {
|
||||
layouts.append(&mut columns(
|
||||
&Rect {
|
||||
left: area.left,
|
||||
top: stack_top,
|
||||
right: area.right,
|
||||
bottom: area.bottom - bottom,
|
||||
},
|
||||
len - 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let primary_right = match len {
|
||||
1 => area.right,
|
||||
_ => area.right / 2,
|
||||
};
|
||||
|
||||
let secondary_right = match len {
|
||||
1 => 0,
|
||||
2 => area.right - primary_right,
|
||||
_ => (area.right - primary_right) / 2,
|
||||
};
|
||||
|
||||
let (primary_left, secondary_left, stack_left) = match len {
|
||||
1 => (area.left, 0, 0),
|
||||
2 => {
|
||||
let mut primary = area.left + secondary_right;
|
||||
let mut secondary = area.left;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
primary = area.left;
|
||||
secondary = area.left + primary_right;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(primary, secondary, 0)
|
||||
}
|
||||
_ => {
|
||||
let primary = area.left + secondary_right;
|
||||
let mut secondary = area.left;
|
||||
let mut stack = area.left + primary_right + secondary_right;
|
||||
|
||||
match layout_flip {
|
||||
Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => {
|
||||
secondary = area.left + primary_right + secondary_right;
|
||||
stack = area.left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(primary, secondary, stack)
|
||||
}
|
||||
};
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: primary_left,
|
||||
top: area.top,
|
||||
right: primary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len >= 2 {
|
||||
layouts.push(Rect {
|
||||
left: secondary_left,
|
||||
top: area.top,
|
||||
right: secondary_right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len > 2 {
|
||||
layouts.append(&mut rows(
|
||||
&Rect {
|
||||
left: stack_left,
|
||||
top: area.top,
|
||||
right: secondary_right,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
len - 2,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
};
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
|
||||
dimensions
|
||||
}
|
||||
}
|
||||
|
||||
impl Arrangement for CustomLayout {
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
_layout_flip: Option<Flip>,
|
||||
_resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let mut dimensions = vec![];
|
||||
let container_count = len.get();
|
||||
|
||||
if container_count <= self.len() {
|
||||
let mut layouts = columns(area, container_count);
|
||||
dimensions.append(&mut layouts);
|
||||
} else {
|
||||
let count_map = self.column_container_counts();
|
||||
|
||||
// If there are not enough windows to trigger the final tertiary
|
||||
// column in the custom layout, use an offset to reduce the number of
|
||||
// columns to calculate each column's area by, so that we don't have
|
||||
// an empty ghost tertiary column and the screen space can be maximised
|
||||
// until there are enough windows to create it
|
||||
let mut tertiary_trigger_threshold = 0;
|
||||
|
||||
// always -1 because we don't insert the tertiary column in the count_map
|
||||
for i in 0..self.len() - 1 {
|
||||
tertiary_trigger_threshold += count_map.get(&i).unwrap();
|
||||
}
|
||||
|
||||
let enable_tertiary_column = len.get() > tertiary_trigger_threshold;
|
||||
|
||||
let offset = if enable_tertiary_column {
|
||||
None
|
||||
} else {
|
||||
Option::from(1)
|
||||
};
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||
let primary_right = self.primary_width_percentage().map_or_else(
|
||||
|| area.right / self.len() as i32,
|
||||
|percentage| (area.right / 100) * percentage as i32,
|
||||
);
|
||||
|
||||
for (idx, column) in self.iter().enumerate() {
|
||||
// If we are offsetting a tertiary column for which the threshold
|
||||
// has not yet been met, this loop should not run for that final
|
||||
// tertiary column
|
||||
if idx < self.len() - offset.unwrap_or(0) {
|
||||
let column_area = if idx == 0 {
|
||||
Self::column_area_with_last(self.len(), area, primary_right, None, offset)
|
||||
} else {
|
||||
Self::column_area_with_last(
|
||||
self.len(),
|
||||
area,
|
||||
primary_right,
|
||||
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||
offset,
|
||||
)
|
||||
};
|
||||
|
||||
match column {
|
||||
Column::Primary(Option::Some(_)) => {
|
||||
let main_column_area = if idx == 0 {
|
||||
Self::main_column_area(area, primary_right, None)
|
||||
} else {
|
||||
Self::main_column_area(
|
||||
area,
|
||||
primary_right,
|
||||
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||
)
|
||||
};
|
||||
|
||||
dimensions.push(main_column_area);
|
||||
}
|
||||
Column::Primary(None) | Column::Secondary(None) => {
|
||||
dimensions.push(column_area);
|
||||
}
|
||||
Column::Secondary(Some(split)) => match split {
|
||||
ColumnSplitWithCapacity::Horizontal(capacity) => {
|
||||
let mut rows = rows(&column_area, *capacity);
|
||||
dimensions.append(&mut rows);
|
||||
}
|
||||
ColumnSplitWithCapacity::Vertical(capacity) => {
|
||||
let mut columns = columns(&column_area, *capacity);
|
||||
dimensions.append(&mut columns);
|
||||
}
|
||||
},
|
||||
Column::Tertiary(split) => {
|
||||
let column_area = Self::column_area_with_last(
|
||||
self.len(),
|
||||
area,
|
||||
primary_right,
|
||||
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||
offset,
|
||||
);
|
||||
|
||||
let remaining = container_count - tertiary_trigger_threshold;
|
||||
|
||||
match split {
|
||||
ColumnSplit::Horizontal => {
|
||||
let mut rows = rows(&column_area, remaining);
|
||||
dimensions.append(&mut rows);
|
||||
}
|
||||
ColumnSplit::Vertical => {
|
||||
let mut columns = columns(&column_area, remaining);
|
||||
dimensions.append(&mut columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
|
||||
dimensions
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Flip {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
HorizontalAndVertical,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn columns(area: &Rect, len: usize) -> Vec<Rect> {
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let right = area.right / len as i32;
|
||||
let mut left = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left + left,
|
||||
top: area.top,
|
||||
right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
left += right;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn rows(area: &Rect, len: usize) -> Vec<Rect> {
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let bottom = area.bottom / len as i32;
|
||||
let mut top = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left,
|
||||
top: area.top + top,
|
||||
right: area.right,
|
||||
bottom,
|
||||
});
|
||||
|
||||
top += bottom;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
||||
let mut resize_adjustments = resize_dimensions.to_vec();
|
||||
|
||||
// This needs to be aware of layout flips
|
||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(resize_ref) = opt {
|
||||
if i > 0 {
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
|
||||
.iter()
|
||||
.map(|adjustment| match adjustment {
|
||||
None => None,
|
||||
Some(rect) if rect.eq(&Rect::default()) => None,
|
||||
Some(_) => *adjustment,
|
||||
})
|
||||
.collect();
|
||||
|
||||
cleaned_resize_adjustments
|
||||
}
|
||||
|
||||
fn recursive_fibonacci(
|
||||
idx: usize,
|
||||
count: usize,
|
||||
area: &Rect,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_adjustments: Vec<Option<Rect>>,
|
||||
) -> Vec<Rect> {
|
||||
let mut a = *area;
|
||||
|
||||
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
|
||||
a.left += r.left;
|
||||
a.top += r.top;
|
||||
a.right += r.right;
|
||||
a.bottom += r.bottom;
|
||||
a
|
||||
} else {
|
||||
*area
|
||||
};
|
||||
|
||||
let half_width = area.right / 2;
|
||||
let half_height = area.bottom / 2;
|
||||
let half_resized_width = resized.right / 2;
|
||||
let half_resized_height = resized.bottom / 2;
|
||||
|
||||
let (main_x, alt_x, alt_y, main_y);
|
||||
|
||||
if let Some(flip) = layout_flip {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
|
||||
alt_y = resized.top + half_resized_height;
|
||||
main_y = resized.top;
|
||||
}
|
||||
Flip::Vertical => {
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
}
|
||||
Flip::HorizontalAndVertical => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
main_y = resized.top;
|
||||
alt_y = resized.top + half_resized_height;
|
||||
}
|
||||
|
||||
#[allow(clippy::if_not_else)]
|
||||
if count == 0 {
|
||||
vec![]
|
||||
} else if count == 1 {
|
||||
vec![Rect {
|
||||
left: resized.left,
|
||||
top: resized.top,
|
||||
right: resized.right,
|
||||
bottom: resized.bottom,
|
||||
}]
|
||||
} else if idx % 2 != 0 {
|
||||
let mut res = vec![Rect {
|
||||
left: resized.left,
|
||||
top: main_y,
|
||||
right: resized.right,
|
||||
bottom: half_resized_height,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: area.left,
|
||||
top: alt_y,
|
||||
right: area.right,
|
||||
bottom: area.bottom - half_resized_height,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
} else {
|
||||
let mut res = vec![Rect {
|
||||
left: main_x,
|
||||
top: resized.top,
|
||||
right: half_resized_width,
|
||||
bottom: resized.bottom,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: alt_x,
|
||||
top: area.top,
|
||||
right: area.right - half_resized_width,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
}
|
||||
}
|
||||
262
komorebi-core/src/custom_layout.rs
Normal file
262
komorebi-core/src/custom_layout.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CustomLayout(Vec<Column>);
|
||||
|
||||
impl Deref for CustomLayout {
|
||||
type Target = Vec<Column>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomLayout {
|
||||
pub fn from_path_buf(path: PathBuf) -> Result<Self> {
|
||||
let invalid_filetype = anyhow!("custom layouts must be json or yaml files");
|
||||
let layout: Self = match path.extension() {
|
||||
Some(extension) => {
|
||||
if extension == "yaml" || extension == "yml" {
|
||||
serde_yaml::from_reader(BufReader::new(File::open(path)?))?
|
||||
} else if extension == "json" {
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
} else {
|
||||
return Err(invalid_filetype);
|
||||
}
|
||||
}
|
||||
None => return Err(invalid_filetype),
|
||||
};
|
||||
|
||||
if !layout.is_valid() {
|
||||
return Err(anyhow!("the layout file provided was invalid"));
|
||||
}
|
||||
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_with_idx(&self, idx: usize) -> (usize, Option<&Column>) {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
let column = self.get(column_idx);
|
||||
(column_idx, column)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn primary_idx(&self) -> Option<usize> {
|
||||
for (i, column) in self.iter().enumerate() {
|
||||
if let Column::Primary(_) = column {
|
||||
return Option::from(i);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn primary_width_percentage(&self) -> Option<usize> {
|
||||
for column in self.iter() {
|
||||
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(percentage))) = column
|
||||
{
|
||||
return Option::from(*percentage);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
// A valid layout must have at least one column
|
||||
if self.is_empty() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Vertical column splits aren't supported at the moment
|
||||
for column in self.iter() {
|
||||
match column {
|
||||
Column::Tertiary(ColumnSplit::Vertical)
|
||||
| Column::Secondary(Some(ColumnSplitWithCapacity::Vertical(_))) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// The final column must not have a fixed capacity
|
||||
match self.last() {
|
||||
Some(Column::Tertiary(_)) => {}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
let mut primaries = 0;
|
||||
let mut tertiaries = 0;
|
||||
|
||||
for column in self.iter() {
|
||||
match column {
|
||||
Column::Primary(_) => primaries += 1,
|
||||
Column::Tertiary(_) => tertiaries += 1,
|
||||
Column::Secondary(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// There must only be one primary and one tertiary column
|
||||
matches!(primaries, 1) && matches!(tertiaries, 1)
|
||||
}
|
||||
|
||||
pub(crate) fn column_container_counts(&self) -> HashMap<usize, usize> {
|
||||
let mut count_map = HashMap::new();
|
||||
|
||||
for (idx, column) in self.iter().enumerate() {
|
||||
match column {
|
||||
Column::Primary(_) | Column::Secondary(None) => {
|
||||
count_map.insert(idx, 1);
|
||||
}
|
||||
Column::Secondary(Some(split)) => {
|
||||
count_map.insert(
|
||||
idx,
|
||||
match split {
|
||||
ColumnSplitWithCapacity::Vertical(n)
|
||||
| ColumnSplitWithCapacity::Horizontal(n) => *n,
|
||||
},
|
||||
);
|
||||
}
|
||||
Column::Tertiary(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
count_map
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn first_container_idx(&self, col_idx: usize) -> usize {
|
||||
let count_map = self.column_container_counts();
|
||||
let mut container_idx_accumulator = 0;
|
||||
|
||||
for i in 0..col_idx {
|
||||
if let Some(n) = count_map.get(&i) {
|
||||
container_idx_accumulator += n;
|
||||
}
|
||||
}
|
||||
|
||||
container_idx_accumulator
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_for_container_idx(&self, idx: usize) -> usize {
|
||||
let count_map = self.column_container_counts();
|
||||
let mut container_idx_accumulator = 0;
|
||||
|
||||
// always -1 because we don't insert the tertiary column in the count_map
|
||||
for i in 0..self.len() - 1 {
|
||||
if let Some(n) = count_map.get(&i) {
|
||||
container_idx_accumulator += n;
|
||||
|
||||
// The accumulator becomes greater than the window container index
|
||||
// for the first time when we reach a column that contains that
|
||||
// window container index
|
||||
if container_idx_accumulator > idx {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the accumulator never reaches a point where it is greater than the
|
||||
// window container index, then the only remaining possibility is the
|
||||
// final tertiary column
|
||||
self.len() - 1
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_area(&self, work_area: &Rect, idx: usize, offset: Option<usize>) -> Rect {
|
||||
let divisor = offset.map_or_else(|| self.len(), |offset| self.len() - offset);
|
||||
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let equal_width = work_area.right / divisor as i32;
|
||||
let mut left = work_area.left;
|
||||
let right = equal_width;
|
||||
|
||||
for _ in 0..idx {
|
||||
left += right;
|
||||
}
|
||||
|
||||
Rect {
|
||||
left,
|
||||
top: work_area.top,
|
||||
right,
|
||||
bottom: work_area.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn column_area_with_last(
|
||||
len: usize,
|
||||
work_area: &Rect,
|
||||
primary_right: i32,
|
||||
last_column: Option<Rect>,
|
||||
offset: Option<usize>,
|
||||
) -> Rect {
|
||||
let divisor = offset.map_or_else(|| len - 1, |offset| len - offset - 1);
|
||||
|
||||
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||
let equal_width = (work_area.right - primary_right) / divisor as i32;
|
||||
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
|
||||
let right = equal_width;
|
||||
|
||||
Rect {
|
||||
left,
|
||||
top: work_area.top,
|
||||
right,
|
||||
bottom: work_area.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn main_column_area(
|
||||
work_area: &Rect,
|
||||
primary_right: i32,
|
||||
last_column: Option<Rect>,
|
||||
) -> Rect {
|
||||
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
|
||||
|
||||
Rect {
|
||||
left,
|
||||
top: work_area.top,
|
||||
right: primary_right,
|
||||
bottom: work_area.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "column", content = "configuration")]
|
||||
pub enum Column {
|
||||
Primary(Option<ColumnWidth>),
|
||||
Secondary(Option<ColumnSplitWithCapacity>),
|
||||
Tertiary(ColumnSplit),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum ColumnWidth {
|
||||
WidthPercentage(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum ColumnSplit {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum ColumnSplitWithCapacity {
|
||||
Horizontal(usize),
|
||||
Vertical(usize),
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -13,17 +15,17 @@ pub enum CycleDirection {
|
||||
|
||||
impl CycleDirection {
|
||||
#[must_use]
|
||||
pub const fn next_idx(&self, idx: usize, len: usize) -> usize {
|
||||
pub const fn next_idx(&self, idx: usize, len: NonZeroUsize) -> usize {
|
||||
match self {
|
||||
CycleDirection::Previous => {
|
||||
Self::Previous => {
|
||||
if idx == 0 {
|
||||
len - 1
|
||||
len.get() - 1
|
||||
} else {
|
||||
idx - 1
|
||||
}
|
||||
}
|
||||
CycleDirection::Next => {
|
||||
if idx == len - 1 {
|
||||
Self::Next => {
|
||||
if idx == len.get() - 1 {
|
||||
0
|
||||
} else {
|
||||
idx + 1
|
||||
|
||||
125
komorebi-core/src/default_layout.rs
Normal file
125
komorebi-core/src/default_layout.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::OperationDirection;
|
||||
use crate::Rect;
|
||||
use crate::Sizing;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum DefaultLayout {
|
||||
BSP,
|
||||
Columns,
|
||||
Rows,
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn resize(
|
||||
&self,
|
||||
unaltered: &Rect,
|
||||
resize: &Option<Rect>,
|
||||
edge: OperationDirection,
|
||||
sizing: Sizing,
|
||||
step: Option<i32>,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(self, Self::BSP) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let max_divisor = 1.005;
|
||||
let mut r = resize.unwrap_or_default();
|
||||
|
||||
let resize_step = step.unwrap_or(50);
|
||||
|
||||
match edge {
|
||||
OperationDirection::Left => match sizing {
|
||||
Sizing::Increase => {
|
||||
// Some final checks to make sure the user can't infinitely resize to
|
||||
// the point of pushing other windows out of bounds
|
||||
|
||||
// Note: These checks cannot take into account the changes made to the
|
||||
// edges of adjacent windows at operation time, so it is still possible
|
||||
// to push windows out of bounds by maxing out an Increase Left on a
|
||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||
// with index 0. I don't think it's worth trying to defensively program
|
||||
// against this; if people end up in this situation they are better off
|
||||
// just hitting the retile command
|
||||
let diff = ((r.left + -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.left - -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Up => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.top + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.top - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.right + resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.right - resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Down => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.bottom + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.bottom - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if r.eq(&Rect::default()) {
|
||||
None
|
||||
} else {
|
||||
Option::from(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
289
komorebi-core/src/direction.rs
Normal file
289
komorebi-core/src/direction.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::custom_layout::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::OperationDirection;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> Option<usize>;
|
||||
|
||||
fn is_valid_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool;
|
||||
fn up_index(&self, idx: usize) -> usize;
|
||||
fn down_index(&self, idx: usize) -> usize;
|
||||
fn left_index(&self, idx: usize) -> usize;
|
||||
fn right_index(&self, idx: usize) -> usize;
|
||||
}
|
||||
|
||||
impl Direction for DefaultLayout {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> Option<usize> {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool {
|
||||
match op_direction {
|
||||
OperationDirection::Up => match self {
|
||||
DefaultLayout::BSP => count > 2 && idx != 0 && idx != 1,
|
||||
DefaultLayout::Columns => false,
|
||||
DefaultLayout::Rows | DefaultLayout::HorizontalStack => idx != 0,
|
||||
DefaultLayout::VerticalStack => idx != 0 && idx != 1,
|
||||
DefaultLayout::UltrawideVerticalStack => idx > 2,
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
DefaultLayout::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
|
||||
DefaultLayout::Columns => false,
|
||||
DefaultLayout::Rows => idx != count - 1,
|
||||
DefaultLayout::VerticalStack => idx != 0 && idx != count - 1,
|
||||
DefaultLayout::HorizontalStack => idx == 0,
|
||||
DefaultLayout::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
DefaultLayout::BSP => count > 1 && idx != 0,
|
||||
DefaultLayout::Columns | DefaultLayout::VerticalStack => idx != 0,
|
||||
DefaultLayout::Rows => false,
|
||||
DefaultLayout::HorizontalStack => idx != 0 && idx != 1,
|
||||
DefaultLayout::UltrawideVerticalStack => count > 1 && idx != 1,
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
DefaultLayout::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
|
||||
DefaultLayout::Columns => idx != count - 1,
|
||||
DefaultLayout::Rows => false,
|
||||
DefaultLayout::VerticalStack => idx == 0,
|
||||
DefaultLayout::HorizontalStack => idx != 0 && idx != count - 1,
|
||||
DefaultLayout::UltrawideVerticalStack => match count {
|
||||
0 | 1 => false,
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 1
|
||||
} else {
|
||||
idx - 2
|
||||
}
|
||||
}
|
||||
DefaultLayout::Columns => unreachable!(),
|
||||
DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => idx - 1,
|
||||
DefaultLayout::HorizontalStack => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => idx + 1,
|
||||
DefaultLayout::Columns => unreachable!(),
|
||||
DefaultLayout::HorizontalStack => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 2
|
||||
} else {
|
||||
idx - 1
|
||||
}
|
||||
}
|
||||
DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx - 1,
|
||||
DefaultLayout::Rows => unreachable!(),
|
||||
DefaultLayout::VerticalStack => 0,
|
||||
DefaultLayout::UltrawideVerticalStack => match idx {
|
||||
0 => 1,
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP | DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx + 1,
|
||||
DefaultLayout::Rows => unreachable!(),
|
||||
DefaultLayout::VerticalStack => 1,
|
||||
DefaultLayout::UltrawideVerticalStack => match idx {
|
||||
1 => 0,
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Direction for CustomLayout {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> Option<usize> {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => idx != 0 && self.column_for_container_idx(idx) != 0,
|
||||
OperationDirection::Right => {
|
||||
idx != count - 1 && self.column_for_container_idx(idx) != self.len() - 1
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if idx == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (column_idx, column) = self.column_with_idx(idx);
|
||||
match column {
|
||||
None => false,
|
||||
Some(column) => match column {
|
||||
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
|
||||
| Column::Tertiary(ColumnSplit::Horizontal) => {
|
||||
self.column_for_container_idx(idx - 1) == column_idx
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if idx == count - 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (column_idx, column) = self.column_with_idx(idx);
|
||||
match column {
|
||||
None => false,
|
||||
Some(column) => match column {
|
||||
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
|
||||
| Column::Tertiary(ColumnSplit::Horizontal) => {
|
||||
self.column_for_container_idx(idx + 1) == column_idx
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
idx - 1
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
idx + 1
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
if column_idx - 1 == 0 {
|
||||
0
|
||||
} else {
|
||||
self.first_container_idx(column_idx - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
self.first_container_idx(column_idx + 1)
|
||||
}
|
||||
}
|
||||
@@ -1,388 +1,31 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::OperationDirection;
|
||||
use crate::Rect;
|
||||
use crate::Sizing;
|
||||
use crate::Arrangement;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Direction;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Layout {
|
||||
BSP,
|
||||
Columns,
|
||||
Rows,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Flip {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
HorizontalAndVertical,
|
||||
Default(DefaultLayout),
|
||||
Custom(CustomLayout),
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn resize(
|
||||
&self,
|
||||
unaltered: &Rect,
|
||||
resize: &Option<Rect>,
|
||||
edge: OperationDirection,
|
||||
sizing: Sizing,
|
||||
step: Option<i32>,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(self, Self::BSP) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let max_divisor = 1.005;
|
||||
let mut r = resize.unwrap_or_default();
|
||||
|
||||
let resize_step = step.unwrap_or(50);
|
||||
|
||||
match edge {
|
||||
OperationDirection::Left => match sizing {
|
||||
Sizing::Increase => {
|
||||
// Some final checks to make sure the user can't infinitely resize to
|
||||
// the point of pushing other windows out of bounds
|
||||
|
||||
// Note: These checks cannot take into account the changes made to the
|
||||
// edges of adjacent windows at operation time, so it is still possible
|
||||
// to push windows out of bounds by maxing out an Increase Left on a
|
||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||
// with index 0. I don't think it's worth trying to defensively program
|
||||
// against this; if people end up in this situation they are better off
|
||||
// just hitting the retile command
|
||||
let diff = ((r.left + -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.left - -resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.left -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Up => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.top + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top += -resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.top - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.top -= -resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.right + resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.right - resize_step) as f32).abs();
|
||||
let max = unaltered.right as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.right -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Down => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.bottom + resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom += resize_step;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.bottom - resize_step) as f32).abs();
|
||||
let max = unaltered.bottom as f32 / max_divisor;
|
||||
if diff < max {
|
||||
r.bottom -= resize_step;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if r.eq(&Rect::default()) {
|
||||
None
|
||||
} else {
|
||||
Option::from(r)
|
||||
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
||||
match self {
|
||||
Layout::Default(layout) => Box::new(*layout),
|
||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||
pub fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
len: NonZeroUsize,
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
Layout::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
area,
|
||||
layout_flip,
|
||||
calculate_resize_adjustments(resize_dimensions),
|
||||
),
|
||||
Layout::Columns => {
|
||||
let right = area.right / len as i32;
|
||||
let mut left = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left + left,
|
||||
top: area.top,
|
||||
right,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
left += right;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::Rows => {
|
||||
let bottom = area.bottom / len as i32;
|
||||
let mut top = 0;
|
||||
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
for _ in 0..len {
|
||||
layouts.push(Rect {
|
||||
left: area.left,
|
||||
top: area.top + top,
|
||||
right: area.right,
|
||||
bottom,
|
||||
});
|
||||
|
||||
top += bottom;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
};
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
|
||||
dimensions
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
||||
let mut resize_adjustments = resize_dimensions.to_vec();
|
||||
|
||||
// This needs to be aware of layout flips
|
||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(resize_ref) = opt {
|
||||
if i > 0 {
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
||||
match self {
|
||||
Layout::Default(layout) => Box::new(*layout),
|
||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
|
||||
.iter()
|
||||
.map(|adjustment| match adjustment {
|
||||
None => None,
|
||||
Some(rect) if rect.eq(&Rect::default()) => None,
|
||||
Some(_) => *adjustment,
|
||||
})
|
||||
.collect();
|
||||
|
||||
cleaned_resize_adjustments
|
||||
}
|
||||
|
||||
fn recursive_fibonacci(
|
||||
idx: usize,
|
||||
count: usize,
|
||||
area: &Rect,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_adjustments: Vec<Option<Rect>>,
|
||||
) -> Vec<Rect> {
|
||||
let mut a = *area;
|
||||
|
||||
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
|
||||
a.left += r.left;
|
||||
a.top += r.top;
|
||||
a.right += r.right;
|
||||
a.bottom += r.bottom;
|
||||
a
|
||||
} else {
|
||||
*area
|
||||
};
|
||||
|
||||
let half_width = area.right / 2;
|
||||
let half_height = area.bottom / 2;
|
||||
let half_resized_width = resized.right / 2;
|
||||
let half_resized_height = resized.bottom / 2;
|
||||
|
||||
let (main_x, alt_x, alt_y, main_y);
|
||||
|
||||
if let Some(flip) = layout_flip {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
|
||||
alt_y = resized.top + half_resized_height;
|
||||
main_y = resized.top;
|
||||
}
|
||||
Flip::Vertical => {
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
}
|
||||
Flip::HorizontalAndVertical => {
|
||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||
alt_x = resized.left;
|
||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||
alt_y = resized.top;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
main_y = resized.top;
|
||||
alt_y = resized.top + half_resized_height;
|
||||
}
|
||||
|
||||
#[allow(clippy::if_not_else)]
|
||||
if count == 0 {
|
||||
vec![]
|
||||
} else if count == 1 {
|
||||
vec![Rect {
|
||||
left: resized.left,
|
||||
top: resized.top,
|
||||
right: resized.right,
|
||||
bottom: resized.bottom,
|
||||
}]
|
||||
} else if idx % 2 != 0 {
|
||||
let mut res = vec![Rect {
|
||||
left: resized.left,
|
||||
top: main_y,
|
||||
right: resized.right,
|
||||
bottom: half_resized_height,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: area.left,
|
||||
top: alt_y,
|
||||
right: area.right,
|
||||
bottom: area.bottom - half_resized_height,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
} else {
|
||||
let mut res = vec![Rect {
|
||||
left: main_x,
|
||||
top: resized.top,
|
||||
right: half_resized_width,
|
||||
bottom: resized.bottom,
|
||||
}];
|
||||
res.append(&mut recursive_fibonacci(
|
||||
idx + 1,
|
||||
count - 1,
|
||||
&Rect {
|
||||
left: alt_x,
|
||||
top: area.top,
|
||||
right: area.right - half_resized_width,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
layout_flip,
|
||||
resize_adjustments,
|
||||
));
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,33 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Flip;
|
||||
pub use custom_layout::CustomLayout;
|
||||
pub use cycle_direction::CycleDirection;
|
||||
pub use layout::Flip;
|
||||
pub use default_layout::DefaultLayout;
|
||||
pub use direction::Direction;
|
||||
pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod arrangement;
|
||||
pub mod custom_layout;
|
||||
pub mod cycle_direction;
|
||||
pub mod default_layout;
|
||||
pub mod direction;
|
||||
pub mod layout;
|
||||
pub mod operation_direction;
|
||||
pub mod rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum SocketMessage {
|
||||
// Window / Container Commands
|
||||
FocusWindow(OperationDirection),
|
||||
MoveWindow(OperationDirection),
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
ResizeWindow(OperationDirection, Sizing),
|
||||
UnstackWindow,
|
||||
@@ -44,7 +55,8 @@ pub enum SocketMessage {
|
||||
UnmanageFocusedWindow,
|
||||
AdjustContainerPadding(Sizing, i32),
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(Layout),
|
||||
ChangeLayout(DefaultLayout),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Flip),
|
||||
// Monitor and Workspace Commands
|
||||
EnsureWorkspaces(usize, usize),
|
||||
@@ -57,36 +69,40 @@ pub enum SocketMessage {
|
||||
QuickLoad,
|
||||
Save(PathBuf),
|
||||
Load(PathBuf),
|
||||
CycleFocusMonitor(CycleDirection),
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusWorkspaceNumber(usize),
|
||||
ContainerPadding(usize, usize, i32),
|
||||
WorkspacePadding(usize, usize, i32),
|
||||
WorkspaceTiling(usize, usize, bool),
|
||||
WorkspaceName(usize, usize, String),
|
||||
WorkspaceLayout(usize, usize, Layout),
|
||||
WorkspaceLayout(usize, usize, DefaultLayout),
|
||||
WorkspaceLayoutCustom(usize, usize, PathBuf),
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
WatchConfiguration(bool),
|
||||
InvisibleBorders(Rect),
|
||||
WorkAreaOffset(Rect),
|
||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||
IdentifyBorderOverflow(ApplicationIdentifier, String),
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
State,
|
||||
Query(StateQuery),
|
||||
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
|
||||
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
|
||||
AddSubscriber(String),
|
||||
RemoveSubscriber(String),
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
Ok(serde_json::from_slice(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SocketMessage {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use clap::ArgEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::direction::Direction;
|
||||
use crate::Flip;
|
||||
use crate::Layout;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
@@ -27,92 +29,35 @@ impl OperationDirection {
|
||||
}
|
||||
}
|
||||
|
||||
fn flip_direction(direction: Self, layout_flip: Option<Flip>) -> Self {
|
||||
layout_flip.map_or(direction, |flip| match direction {
|
||||
fn flip(self, layout_flip: Option<Flip>) -> Self {
|
||||
layout_flip.map_or(self, |flip| match self {
|
||||
Self::Left => match flip {
|
||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Right,
|
||||
Flip::Vertical => direction,
|
||||
Flip::Vertical => self,
|
||||
},
|
||||
Self::Right => match flip {
|
||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Left,
|
||||
Flip::Vertical => direction,
|
||||
Flip::Vertical => self,
|
||||
},
|
||||
Self::Up => match flip {
|
||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Down,
|
||||
Flip::Horizontal => direction,
|
||||
Flip::Horizontal => self,
|
||||
},
|
||||
Self::Down => match flip {
|
||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Up,
|
||||
Flip::Horizontal => direction,
|
||||
Flip::Horizontal => self,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid(
|
||||
pub fn destination(
|
||||
self,
|
||||
layout: Layout,
|
||||
layout: &dyn Direction,
|
||||
layout_flip: Option<Flip>,
|
||||
idx: usize,
|
||||
len: usize,
|
||||
) -> bool {
|
||||
match Self::flip_direction(self, layout_flip) {
|
||||
OperationDirection::Up => match layout {
|
||||
Layout::BSP => len > 2 && idx != 0 && idx != 1,
|
||||
Layout::Columns => false,
|
||||
Layout::Rows => idx != 0,
|
||||
},
|
||||
OperationDirection::Down => match layout {
|
||||
Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0,
|
||||
Layout::Columns => false,
|
||||
Layout::Rows => idx != len - 1,
|
||||
},
|
||||
OperationDirection::Left => match layout {
|
||||
Layout::BSP => len > 1 && idx != 0,
|
||||
Layout::Columns => idx != 0,
|
||||
Layout::Rows => false,
|
||||
},
|
||||
OperationDirection::Right => match layout {
|
||||
Layout::BSP => len > 1 && idx % 2 == 0 && idx != len - 1,
|
||||
Layout::Columns => idx != len - 1,
|
||||
Layout::Rows => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_idx(self, layout: Layout, layout_flip: Option<Flip>, idx: usize) -> usize {
|
||||
match Self::flip_direction(self, layout_flip) {
|
||||
Self::Up => match layout {
|
||||
Layout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 1
|
||||
} else {
|
||||
idx - 2
|
||||
}
|
||||
}
|
||||
Layout::Columns => unreachable!(),
|
||||
Layout::Rows => idx - 1,
|
||||
},
|
||||
Self::Down => match layout {
|
||||
Layout::BSP | Layout::Rows => idx + 1,
|
||||
Layout::Columns => unreachable!(),
|
||||
},
|
||||
Self::Left => match layout {
|
||||
Layout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 2
|
||||
} else {
|
||||
idx - 1
|
||||
}
|
||||
}
|
||||
Layout::Columns => idx - 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
},
|
||||
Self::Right => match layout {
|
||||
Layout::BSP | Layout::Columns => idx + 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
},
|
||||
}
|
||||
len: NonZeroUsize,
|
||||
) -> Option<usize> {
|
||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
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"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -20,7 +20,7 @@ color-eyre = "0.5"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
ctrlc = "3"
|
||||
dirs = "3"
|
||||
dirs = "4"
|
||||
getset = "0.1"
|
||||
hotwatch = "0.4"
|
||||
lazy_static = "1"
|
||||
@@ -38,6 +38,7 @@ uds_windows = "1"
|
||||
which = "4"
|
||||
winput = "0.2"
|
||||
winvd = "0.0.20"
|
||||
miow = "0.3"
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -20,15 +22,19 @@ use lazy_static::lazy_static;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use sysinfo::SystemExt;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use which::which;
|
||||
|
||||
use komorebi_core::SocketMessage;
|
||||
|
||||
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::State;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
@@ -74,9 +80,20 @@ lazy_static! {
|
||||
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![]));
|
||||
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"X410.exe".to_string(),
|
||||
"mstsc.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
}
|
||||
|
||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
@@ -177,6 +194,53 @@ pub fn load_configuration() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_subscriptions = vec![];
|
||||
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in subscriptions.iter_mut() {
|
||||
match writeln!(pipe, "{}", notification) {
|
||||
Ok(_) => {
|
||||
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||
}
|
||||
Err(error) => {
|
||||
// ERROR_FILE_NOT_FOUND
|
||||
// 2 (0x2)
|
||||
// The system cannot find the file specified.
|
||||
|
||||
// ERROR_NO_DATA
|
||||
// 232 (0xE8)
|
||||
// The pipe is being closed.
|
||||
|
||||
// Remove the subscription; the process will have to subscribe again
|
||||
if let Some(2 | 232) = error.raw_os_error() {
|
||||
let subscriber_cl = subscriber.clone();
|
||||
stale_subscriptions.push(subscriber_cl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for subscriber in stale_subscriptions {
|
||||
tracing::warn!("removing stale subscription: {}", subscriber);
|
||||
subscriptions.remove(&subscriber);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
#[tracing::instrument]
|
||||
fn detect_deadlocks() {
|
||||
@@ -267,7 +331,7 @@ fn main() -> Result<()> {
|
||||
|
||||
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
||||
|
||||
wm.lock().restore_all_windows();
|
||||
wm.lock().restore_all_windows()?;
|
||||
std::process::exit(130);
|
||||
}
|
||||
|
||||
|
||||
@@ -146,12 +146,16 @@ impl Monitor {
|
||||
self.workspaces().len()
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace(&mut self, invisible_borders: &Rect) -> Result<()> {
|
||||
pub fn update_focused_workspace(
|
||||
&mut self,
|
||||
offset: Option<Rect>,
|
||||
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, invisible_borders)?;
|
||||
.update(&work_area, offset, invisible_borders)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
@@ -10,6 +11,7 @@ use std::thread;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use miow::pipe::connect;
|
||||
use parking_lot::Mutex;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
@@ -18,13 +20,19 @@ use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::StateQuery;
|
||||
|
||||
use crate::notify_subscribers;
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
@@ -66,6 +74,12 @@ impl WindowManager {
|
||||
SocketMessage::MoveWindow(direction) => {
|
||||
self.move_container_in_direction(direction)?;
|
||||
}
|
||||
SocketMessage::CycleFocusWindow(direction) => {
|
||||
self.focus_container_in_cycle_direction(direction)?;
|
||||
}
|
||||
SocketMessage::CycleMoveWindow(direction) => {
|
||||
self.move_container_in_cycle_direction(direction)?;
|
||||
}
|
||||
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||
SocketMessage::CycleStack(direction) => {
|
||||
@@ -130,18 +144,57 @@ impl WindowManager {
|
||||
SocketMessage::ToggleTiling => {
|
||||
self.toggle_tiling()?;
|
||||
}
|
||||
SocketMessage::CycleFocusMonitor(direction) => {
|
||||
let monitor_idx = direction.next_idx(
|
||||
self.focused_monitor_idx(),
|
||||
NonZeroUsize::new(self.monitors().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
|
||||
);
|
||||
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.update_focused_workspace(true)?;
|
||||
}
|
||||
SocketMessage::FocusMonitorNumber(monitor_idx) => {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.update_focused_workspace(true)?;
|
||||
}
|
||||
SocketMessage::Retile => self.retile_all()?,
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::ChangeLayoutCustom(path) => self.change_workspace_custom_layout(path)?,
|
||||
SocketMessage::WorkspaceLayoutCustom(monitor_idx, workspace_idx, path) => {
|
||||
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?;
|
||||
}
|
||||
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
|
||||
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
|
||||
}
|
||||
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
|
||||
self.set_workspace_layout(monitor_idx, workspace_idx, layout)?;
|
||||
self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?;
|
||||
}
|
||||
SocketMessage::CycleFocusWorkspace(direction) => {
|
||||
// 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)?;
|
||||
|
||||
let focused_monitor = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
|
||||
let workspaces = focused_monitor.workspaces().len();
|
||||
|
||||
let workspace_idx = direction.next_idx(
|
||||
focused_workspace_idx,
|
||||
NonZeroUsize::new(workspaces)
|
||||
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
|
||||
);
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
@@ -159,7 +212,7 @@ impl WindowManager {
|
||||
tracing::info!(
|
||||
"received stop command, restoring all hidden windows and terminating process"
|
||||
);
|
||||
self.restore_all_windows();
|
||||
self.restore_all_windows()?;
|
||||
std::process::exit(0)
|
||||
}
|
||||
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
|
||||
@@ -172,7 +225,7 @@ impl WindowManager {
|
||||
self.set_workspace_name(monitor_idx, workspace_idx, name)?;
|
||||
}
|
||||
SocketMessage::State => {
|
||||
let state = serde_json::to_string_pretty(&window_manager::State::from(self))?;
|
||||
let state = serde_json::to_string_pretty(&window_manager::State::from(&*self))?;
|
||||
let mut socket =
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
socket.push("komorebic.sock");
|
||||
@@ -329,6 +382,10 @@ impl WindowManager {
|
||||
self.invisible_borders = rect;
|
||||
self.retile_all()?;
|
||||
}
|
||||
SocketMessage::WorkAreaOffset(rect) => {
|
||||
self.work_area_offset = Option::from(rect);
|
||||
self.retile_all()?;
|
||||
}
|
||||
SocketMessage::QuickSave => {
|
||||
let workspace = self.focused_workspace()?;
|
||||
let resize = workspace.resize_dimensions();
|
||||
@@ -385,6 +442,30 @@ impl WindowManager {
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
SocketMessage::AddSubscriber(subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
let pipe_path = format!(r"\\.\pipe\{}", subscriber);
|
||||
let pipe = connect(&pipe_path).map_err(|_| {
|
||||
anyhow!("the named pipe '{}' has not yet been created; please create it before running this command", pipe_path)
|
||||
})?;
|
||||
|
||||
pipes.insert(subscriber, pipe);
|
||||
}
|
||||
SocketMessage::RemoveSubscriber(subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
pipes.remove(&subscriber);
|
||||
}
|
||||
SocketMessage::RemoveTitleBar(_, id) => {
|
||||
let mut identifiers = NO_TITLEBAR.lock();
|
||||
if !identifiers.contains(&id) {
|
||||
identifiers.push(id);
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleTitleBars => {
|
||||
let current = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
REMOVE_TITLEBARS.store(!current, Ordering::SeqCst);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("processed");
|
||||
@@ -409,7 +490,11 @@ impl WindowManager {
|
||||
};
|
||||
}
|
||||
|
||||
self.process_command(message)?;
|
||||
self.process_command(message.clone())?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: (&*self).into(),
|
||||
})?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -11,9 +11,12 @@ use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
|
||||
use crate::notify_subscribers;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
|
||||
@@ -66,13 +69,14 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
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, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
@@ -119,10 +123,10 @@ impl WindowManager {
|
||||
// they are not on the top of a container stack.
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
|
||||
if (!window.is_window()
|
||||
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)
|
||||
|| tray_and_multi_window_identifiers.contains(&window.class()?))
|
||||
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
{
|
||||
hide = true;
|
||||
}
|
||||
@@ -315,6 +319,10 @@ impl WindowManager {
|
||||
.open(hwnd_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::WindowManager(*event),
|
||||
state: (&*self).into(),
|
||||
})?)?;
|
||||
|
||||
tracing::info!("processed: {}", event.window().to_string());
|
||||
Ok(())
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::fmt::Formatter;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use serde::ser::Error;
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
@@ -20,6 +21,8 @@ use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Window {
|
||||
@@ -55,12 +58,28 @@ impl Serialize for Window {
|
||||
{
|
||||
let mut state = serializer.serialize_struct("Window", 5)?;
|
||||
state.serialize_field("hwnd", &self.hwnd)?;
|
||||
state.serialize_field("title", &self.title().expect("could not get window title"))?;
|
||||
state.serialize_field("exe", &self.exe().expect("could not get window exe"))?;
|
||||
state.serialize_field("class", &self.class().expect("could not get window class"))?;
|
||||
state.serialize_field(
|
||||
"title",
|
||||
&self
|
||||
.title()
|
||||
.map_err(|_| S::Error::custom("could not get window title"))?,
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"exe",
|
||||
&self
|
||||
.exe()
|
||||
.map_err(|_| S::Error::custom("could not get window exe"))?,
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"class",
|
||||
&self
|
||||
.class()
|
||||
.map_err(|_| S::Error::custom("could not get window class"))?,
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"rect",
|
||||
&WindowsApi::window_rect(self.hwnd()).expect("could not get window rect"),
|
||||
&WindowsApi::window_rect(self.hwnd())
|
||||
.map_err(|_| S::Error::custom("could not get window rect"))?,
|
||||
)?;
|
||||
state.end()
|
||||
}
|
||||
@@ -193,6 +212,20 @@ impl Window {
|
||||
WindowsApi::set_focus(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn remove_title_bar(self) -> Result<()> {
|
||||
let mut style = self.style()?;
|
||||
style.remove(GwlStyle::CAPTION);
|
||||
style.remove(GwlStyle::THICKFRAME);
|
||||
self.update_style(style)
|
||||
}
|
||||
|
||||
pub fn add_title_bar(self) -> Result<()> {
|
||||
let mut style = self.style()?;
|
||||
style.insert(GwlStyle::CAPTION);
|
||||
style.insert(GwlStyle::THICKFRAME);
|
||||
self.update_style(style)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_style(self, style: GwlStyle) -> Result<()> {
|
||||
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||
@@ -235,6 +268,7 @@ impl Window {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
#[allow(clippy::question_mark)]
|
||||
if self.title().is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -271,11 +305,24 @@ impl Window {
|
||||
layered_exe_whitelist.contains(&exe_name)
|
||||
};
|
||||
|
||||
let allow_wsl2_gui = {
|
||||
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
|
||||
wsl2_ui_processes.contains(&exe_name)
|
||||
};
|
||||
|
||||
let allow_titlebar_removed = {
|
||||
let titlebars_removed = NO_TITLEBAR.lock();
|
||||
titlebars_removed.contains(&exe_name)
|
||||
};
|
||||
|
||||
let style = self.style()?;
|
||||
let ex_style = self.ex_style()?;
|
||||
|
||||
if style.contains(GwlStyle::CAPTION)
|
||||
&& ex_style.contains(GwlExStyle::WINDOWEDGE)
|
||||
if (
|
||||
allow_wsl2_gui
|
||||
|| allow_titlebar_removed
|
||||
|| style.contains(GwlStyle::CAPTION) && ex_style.contains(GwlExStyle::WINDOWEDGE)
|
||||
)
|
||||
&& !ex_style.contains(GwlExStyle::DLGMODALFRAME)
|
||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||
// on FocusChange events if I don't filter out this one. But, if we are
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::VecDeque;
|
||||
use std::io::ErrorKind;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
@@ -14,7 +15,10 @@ use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
|
||||
use komorebi_core::custom_layout::CustomLayout;
|
||||
use komorebi_core::Arrangement;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
@@ -35,6 +39,8 @@ use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
@@ -45,6 +51,7 @@ pub struct WindowManager {
|
||||
pub command_listener: UnixListener,
|
||||
pub is_paused: bool,
|
||||
pub invisible_borders: Rect,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub hotwatch: Hotwatch,
|
||||
pub virtual_desktop_id: Option<usize>,
|
||||
@@ -56,8 +63,10 @@ pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub is_paused: bool,
|
||||
pub invisible_borders: Rect,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub remove_titlebars: bool,
|
||||
pub float_identifiers: Vec<String>,
|
||||
pub manage_identifiers: Vec<String>,
|
||||
pub layered_exe_whitelist: Vec<String>,
|
||||
@@ -65,15 +74,16 @@ pub struct State {
|
||||
pub border_overflow_identifiers: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<&mut WindowManager> for State {
|
||||
fn from(wm: &mut WindowManager) -> Self {
|
||||
impl From<&WindowManager> for State {
|
||||
fn from(wm: &WindowManager) -> Self {
|
||||
Self {
|
||||
monitors: wm.monitors.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
invisible_borders: wm.invisible_borders,
|
||||
work_area_offset: wm.work_area_offset,
|
||||
focus_follows_mouse: wm.focus_follows_mouse.clone(),
|
||||
has_pending_raise_op: wm.has_pending_raise_op,
|
||||
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
|
||||
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().clone(),
|
||||
@@ -143,6 +153,7 @@ impl WindowManager {
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
},
|
||||
work_area_offset: None,
|
||||
focus_follows_mouse: None,
|
||||
hotwatch: Hotwatch::new()?,
|
||||
virtual_desktop_id,
|
||||
@@ -265,6 +276,7 @@ impl WindowManager {
|
||||
self.monitors_mut().retain(|m| !invalid.contains(&m.id()));
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
let mut should_update = false;
|
||||
@@ -293,7 +305,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if should_update {
|
||||
monitor.update_focused_workspace(&invisible_borders)?;
|
||||
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +435,8 @@ impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn retile_all(&mut self) -> Result<()> {
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
let work_area = *monitor.work_area_size();
|
||||
let workspace = monitor
|
||||
@@ -434,7 +448,7 @@ impl WindowManager {
|
||||
*resize = None;
|
||||
}
|
||||
|
||||
workspace.update(&work_area, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -534,10 +548,11 @@ impl WindowManager {
|
||||
tracing::info!("updating");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.update_focused_workspace(&invisible_borders)?;
|
||||
.update_focused_workspace(offset, &invisible_borders)?;
|
||||
|
||||
if mouse_follows_focus {
|
||||
if let Some(window) = self.focused_workspace()?.maximized_window() {
|
||||
@@ -556,8 +571,12 @@ impl WindowManager {
|
||||
// Calling this directly instead of the window.focus() wrapper because trying to
|
||||
// attach to the thread of the desktop window always seems to result in "Access is
|
||||
// denied (os error 5)"
|
||||
WindowsApi::set_foreground_window(desktop_window.hwnd())
|
||||
.map_err(|error| anyhow!("{} {}:{}", error, file!(), line!()))?;
|
||||
match WindowsApi::set_foreground_window(desktop_window.hwnd()) {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!("{} {}:{}", error, file!(), line!());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,88 +590,105 @@ impl WindowManager {
|
||||
sizing: Sizing,
|
||||
step: Option<i32>,
|
||||
) -> Result<()> {
|
||||
tracing::info!("resizing window");
|
||||
|
||||
let work_area = self.focused_monitor_work_area()?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let len = workspace.containers().len();
|
||||
let focused_idx = workspace.focused_container_idx();
|
||||
let focused_idx_resize = workspace
|
||||
.resize_dimensions()
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
|
||||
|
||||
if direction.is_valid(
|
||||
workspace.layout(),
|
||||
workspace.layout_flip(),
|
||||
focused_idx,
|
||||
len,
|
||||
) {
|
||||
let unaltered = workspace.layout().calculate(
|
||||
&work_area,
|
||||
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(),
|
||||
&[],
|
||||
);
|
||||
|
||||
let mut direction = direction;
|
||||
|
||||
// We only ever want to operate on the unflipped Rect positions when resizing, then we
|
||||
// can flip them however they need to be flipped once the resizing has been done
|
||||
if let Some(flip) = workspace.layout_flip() {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
if matches!(direction, OperationDirection::Left)
|
||||
|| matches!(direction, OperationDirection::Right)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::Vertical => {
|
||||
if matches!(direction, OperationDirection::Up)
|
||||
|| matches!(direction, OperationDirection::Down)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::HorizontalAndVertical => direction = direction.opposite(),
|
||||
}
|
||||
}
|
||||
|
||||
let resize = workspace.layout().resize(
|
||||
unaltered
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
tracing::info!("resizing window");
|
||||
let len = NonZeroUsize::new(workspace.containers().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one container"))?;
|
||||
let focused_idx = workspace.focused_container_idx();
|
||||
let focused_idx_resize = workspace
|
||||
.resize_dimensions()
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no last layout"))?,
|
||||
focused_idx_resize,
|
||||
direction,
|
||||
sizing,
|
||||
step,
|
||||
);
|
||||
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
|
||||
|
||||
workspace.resize_dimensions_mut()[focused_idx] = resize;
|
||||
self.update_focused_workspace(false)
|
||||
} else {
|
||||
tracing::warn!("cannot resize container in this direction");
|
||||
Ok(())
|
||||
if direction
|
||||
.destination(
|
||||
workspace.layout().as_boxed_direction().as_ref(),
|
||||
workspace.layout_flip(),
|
||||
focused_idx,
|
||||
len,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
let unaltered = layout.calculate(
|
||||
&work_area,
|
||||
len,
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
);
|
||||
|
||||
let mut direction = direction;
|
||||
|
||||
// We only ever want to operate on the unflipped Rect positions when resizing, then we
|
||||
// can flip them however they need to be flipped once the resizing has been done
|
||||
if let Some(flip) = workspace.layout_flip() {
|
||||
match flip {
|
||||
Flip::Horizontal => {
|
||||
if matches!(direction, OperationDirection::Left)
|
||||
|| matches!(direction, OperationDirection::Right)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::Vertical => {
|
||||
if matches!(direction, OperationDirection::Up)
|
||||
|| matches!(direction, OperationDirection::Down)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
Flip::HorizontalAndVertical => direction = direction.opposite(),
|
||||
}
|
||||
}
|
||||
|
||||
let resize = layout.resize(
|
||||
unaltered
|
||||
.get(focused_idx)
|
||||
.ok_or_else(|| anyhow!("there is no last layout"))?,
|
||||
focused_idx_resize,
|
||||
direction,
|
||||
sizing,
|
||||
step,
|
||||
);
|
||||
|
||||
workspace.resize_dimensions_mut()[focused_idx] = resize;
|
||||
return self.update_focused_workspace(false);
|
||||
}
|
||||
|
||||
tracing::warn!("cannot resize container in this direction");
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
tracing::warn!("containers cannot be resized when using custom layouts");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn restore_all_windows(&mut self) {
|
||||
pub fn restore_all_windows(&mut self) -> Result<()> {
|
||||
tracing::info!("restoring all hidden windows");
|
||||
|
||||
let no_titlebar = NO_TITLEBAR.lock();
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
for workspace in monitor.workspaces_mut() {
|
||||
for containers in workspace.containers_mut() {
|
||||
for window in containers.windows_mut() {
|
||||
if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -660,6 +696,7 @@ impl WindowManager {
|
||||
tracing::info!("moving container");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
let monitor = self
|
||||
.focused_monitor_mut()
|
||||
@@ -685,7 +722,7 @@ impl WindowManager {
|
||||
|
||||
target_monitor.add_container(container)?;
|
||||
target_monitor.load_focused_workspace()?;
|
||||
target_monitor.update_focused_workspace(&invisible_borders)?;
|
||||
target_monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
|
||||
if follow {
|
||||
self.focus_monitor(idx)?;
|
||||
@@ -739,18 +776,52 @@ impl WindowManager {
|
||||
self.update_focused_workspace(true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
|
||||
tracing::info!("focusing container");
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let new_idx = workspace
|
||||
.new_idx_for_cycle_direction(direction)
|
||||
.ok_or_else(|| anyhow!("this is not a valid direction from the current position"))?;
|
||||
|
||||
workspace.focus_container(new_idx);
|
||||
self.focused_window_mut()?.focus()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn move_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
|
||||
tracing::info!("moving container");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let current_idx = workspace.focused_container_idx();
|
||||
let new_idx = workspace
|
||||
.new_idx_for_cycle_direction(direction)
|
||||
.ok_or_else(|| anyhow!("this is not a valid direction from the current position"))?;
|
||||
|
||||
workspace.swap_containers(current_idx, new_idx);
|
||||
workspace.focus_container(new_idx);
|
||||
self.update_focused_workspace(true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn cycle_container_window_in_direction(&mut self, direction: CycleDirection) -> Result<()> {
|
||||
tracing::info!("cycling container windows");
|
||||
|
||||
let container = self.focused_container_mut()?;
|
||||
|
||||
if container.windows().len() == 1 {
|
||||
let len = NonZeroUsize::new(container.windows().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
|
||||
|
||||
if len.get() == 1 {
|
||||
return Err(anyhow!("there is only one window in this container"));
|
||||
}
|
||||
|
||||
let current_idx = container.focused_window_idx();
|
||||
let next_idx = direction.next_idx(current_idx, container.windows().len());
|
||||
let next_idx = direction.next_idx(current_idx, len);
|
||||
|
||||
container.focus_window(next_idx);
|
||||
container.load_focused_window();
|
||||
@@ -763,14 +834,18 @@ impl WindowManager {
|
||||
tracing::info!("adding window to container");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let len = NonZeroUsize::new(workspace.containers_mut().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one container"))?;
|
||||
let current_container_idx = workspace.focused_container_idx();
|
||||
|
||||
let is_valid = direction.is_valid(
|
||||
workspace.layout(),
|
||||
workspace.layout_flip(),
|
||||
workspace.focused_container_idx(),
|
||||
workspace.containers_mut().len(),
|
||||
);
|
||||
let is_valid = direction
|
||||
.destination(
|
||||
workspace.layout().as_boxed_direction().as_ref(),
|
||||
workspace.layout_flip(),
|
||||
workspace.focused_container_idx(),
|
||||
len,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if is_valid {
|
||||
let new_idx = workspace.new_idx_for_direction(direction).ok_or_else(|| {
|
||||
@@ -816,7 +891,7 @@ impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn toggle_tiling(&mut self) -> Result<()> {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.set_tile(!workspace.tile());
|
||||
workspace.set_tile(!*workspace.tile());
|
||||
self.update_focused_workspace(false)
|
||||
}
|
||||
|
||||
@@ -971,12 +1046,54 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn change_workspace_layout(&mut self, layout: Layout) -> Result<()> {
|
||||
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
|
||||
tracing::info!("changing layout");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.set_layout(layout);
|
||||
self.update_focused_workspace(false)
|
||||
|
||||
match workspace.layout() {
|
||||
Layout::Default(_) => {}
|
||||
Layout::Custom(layout) => {
|
||||
let primary_idx =
|
||||
layout.first_container_idx(layout.primary_idx().ok_or_else(|| {
|
||||
anyhow!("this custom layout does not have a primary column")
|
||||
})?);
|
||||
|
||||
if !workspace.containers().is_empty() && primary_idx < workspace.containers().len()
|
||||
{
|
||||
workspace.swap_containers(0, primary_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace.set_layout(Layout::Default(layout));
|
||||
self.update_focused_workspace(true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn change_workspace_custom_layout(&mut self, path: PathBuf) -> Result<()> {
|
||||
tracing::info!("changing layout");
|
||||
|
||||
let layout = CustomLayout::from_path_buf(path)?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
match workspace.layout() {
|
||||
Layout::Default(_) => {
|
||||
let primary_idx =
|
||||
layout.first_container_idx(layout.primary_idx().ok_or_else(|| {
|
||||
anyhow!("this custom layout does not have a primary column")
|
||||
})?);
|
||||
|
||||
if !workspace.containers().is_empty() && primary_idx < workspace.containers().len()
|
||||
{
|
||||
workspace.swap_containers(0, primary_idx);
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {}
|
||||
}
|
||||
|
||||
workspace.set_layout(Layout::Custom(layout));
|
||||
self.update_focused_workspace(true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -1032,15 +1149,16 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn set_workspace_layout(
|
||||
pub fn set_workspace_layout_default(
|
||||
&mut self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
layout: Layout,
|
||||
layout: DefaultLayout,
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
let monitor = self
|
||||
@@ -1056,11 +1174,48 @@ impl WindowManager {
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
workspace.set_layout(layout);
|
||||
workspace.set_layout(Layout::Default(layout));
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn set_workspace_layout_custom(
|
||||
&mut self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
path: PathBuf,
|
||||
) -> Result<()> {
|
||||
tracing::info!("setting workspace layout");
|
||||
let layout = CustomLayout::from_path_buf(path)?;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
|
||||
let monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let work_area = *monitor.work_area_size();
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let workspace = monitor
|
||||
.workspaces_mut()
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
workspace.set_layout(Layout::Custom(layout));
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum WindowManagerEvent {
|
||||
Destroy(WinEvent, Window),
|
||||
FocusChange(WinEvent, Window),
|
||||
|
||||
@@ -408,9 +408,9 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowLongPtrW(hwnd, index)
|
||||
}))
|
||||
// Can return 0, which does not always mean that an error has occurred
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
|
||||
Result::from(unsafe { WindowsResult::Ok(GetWindowLongPtrW(hwnd, index)) })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
|
||||
@@ -85,7 +86,7 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Display)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Display)]
|
||||
#[repr(u32)]
|
||||
#[allow(dead_code)]
|
||||
pub enum WinEvent {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
@@ -9,6 +10,8 @@ use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
@@ -18,6 +21,8 @@ use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
|
||||
pub struct Workspace {
|
||||
@@ -36,7 +41,7 @@ pub struct Workspace {
|
||||
maximized_window_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
floating_windows: Vec<Window>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
layout: Layout,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
layout_flip: Option<Flip>,
|
||||
@@ -65,7 +70,7 @@ impl Default for Workspace {
|
||||
maximized_window_restore_idx: None,
|
||||
monocle_container_restore_idx: None,
|
||||
floating_windows: Vec::default(),
|
||||
layout: Layout::BSP,
|
||||
layout: Layout::Default(DefaultLayout::BSP),
|
||||
layout_flip: None,
|
||||
workspace_padding: Option::from(10),
|
||||
container_padding: Option::from(10),
|
||||
@@ -137,8 +142,26 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
|
||||
let mut adjusted_work_area = *work_area;
|
||||
pub fn update(
|
||||
&mut self,
|
||||
work_area: &Rect,
|
||||
offset: Option<Rect>,
|
||||
invisible_borders: &Rect,
|
||||
) -> Result<()> {
|
||||
let container_padding = self.container_padding();
|
||||
let mut adjusted_work_area = offset.map_or_else(
|
||||
|| *work_area,
|
||||
|offset| {
|
||||
let mut with_offset = *work_area;
|
||||
with_offset.left += offset.left;
|
||||
with_offset.top += offset.top;
|
||||
with_offset.right -= offset.right;
|
||||
with_offset.bottom -= offset.bottom;
|
||||
|
||||
with_offset
|
||||
},
|
||||
);
|
||||
|
||||
adjusted_work_area.add_padding(self.workspace_padding());
|
||||
|
||||
self.enforce_resize_constraints();
|
||||
@@ -146,12 +169,13 @@ impl Workspace {
|
||||
if *self.tile() {
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
if let Some(window) = container.focused_window_mut() {
|
||||
adjusted_work_area.add_padding(container_padding);
|
||||
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(
|
||||
let layouts = self.layout().as_boxed_arrangement().calculate(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
@@ -163,9 +187,18 @@ impl Workspace {
|
||||
self.resize_dimensions(),
|
||||
);
|
||||
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
let no_titlebar = { NO_TITLEBAR.lock().clone() };
|
||||
|
||||
let windows = self.visible_windows_mut();
|
||||
for (i, window) in windows.into_iter().enumerate() {
|
||||
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.set_position(layout, invisible_borders, false)?;
|
||||
}
|
||||
}
|
||||
@@ -329,10 +362,19 @@ impl Workspace {
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
self.containers_mut().push_front(container);
|
||||
self.resize_dimensions_mut().insert(0, resize);
|
||||
let primary_idx = match self.layout() {
|
||||
Layout::Default(_) => 0,
|
||||
Layout::Custom(layout) => layout.first_container_idx(
|
||||
layout
|
||||
.primary_idx()
|
||||
.ok_or_else(|| anyhow!("this custom layout does not have a primary column"))?,
|
||||
),
|
||||
};
|
||||
|
||||
self.focus_container(0);
|
||||
self.containers_mut().insert(primary_idx, container);
|
||||
self.resize_dimensions_mut().insert(primary_idx, resize);
|
||||
|
||||
self.focus_container(primary_idx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -442,20 +484,20 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
|
||||
if direction.is_valid(
|
||||
self.layout(),
|
||||
let len = NonZeroUsize::new(self.containers().len())?;
|
||||
|
||||
direction.destination(
|
||||
self.layout().as_boxed_direction().as_ref(),
|
||||
self.layout_flip(),
|
||||
self.focused_container_idx(),
|
||||
self.containers().len(),
|
||||
) {
|
||||
Option::from(direction.new_idx(
|
||||
self.layout(),
|
||||
self.layout_flip(),
|
||||
self.containers.focused_idx(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
len,
|
||||
)
|
||||
}
|
||||
pub fn new_idx_for_cycle_direction(&self, direction: CycleDirection) -> Option<usize> {
|
||||
Option::from(direction.next_idx(
|
||||
self.focused_container_idx(),
|
||||
NonZeroUsize::new(self.containers().len())?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn move_window_to_container(&mut self, target_container_idx: usize) -> Result<()> {
|
||||
|
||||
@@ -16,6 +16,14 @@ Query(state_query) {
|
||||
Run, komorebic.exe query %state_query%, , Hide
|
||||
}
|
||||
|
||||
Subscribe(named_pipe) {
|
||||
Run, komorebic.exe subscribe %named_pipe%, , Hide
|
||||
}
|
||||
|
||||
Unsubscribe(named_pipe) {
|
||||
Run, komorebic.exe unsubscribe %named_pipe%, , Hide
|
||||
}
|
||||
|
||||
Log() {
|
||||
Run, komorebic.exe log, , Hide
|
||||
}
|
||||
@@ -44,6 +52,14 @@ Move(operation_direction) {
|
||||
Run, komorebic.exe move %operation_direction%, , Hide
|
||||
}
|
||||
|
||||
CycleFocus(cycle_direction) {
|
||||
Run, komorebic.exe cycle-focus %cycle_direction%, , Hide
|
||||
}
|
||||
|
||||
CycleMove(cycle_direction) {
|
||||
Run, komorebic.exe cycle-move %cycle_direction%, , Hide
|
||||
}
|
||||
|
||||
Stack(operation_direction) {
|
||||
Run, komorebic.exe stack %operation_direction%, , Hide
|
||||
}
|
||||
@@ -84,6 +100,14 @@ FocusWorkspace(target) {
|
||||
Run, komorebic.exe focus-workspace %target%, , Hide
|
||||
}
|
||||
|
||||
CycleMonitor(cycle_direction) {
|
||||
Run, komorebic.exe cycle-monitor %cycle_direction%, , Hide
|
||||
}
|
||||
|
||||
CycleWorkspace(cycle_direction) {
|
||||
Run, komorebic.exe cycle-workspace %cycle_direction%, , Hide
|
||||
}
|
||||
|
||||
NewWorkspace() {
|
||||
Run, komorebic.exe new-workspace, , Hide
|
||||
}
|
||||
@@ -92,6 +116,10 @@ InvisibleBorders(left, top, right, bottom) {
|
||||
Run, komorebic.exe invisible-borders %left% %top% %right% %bottom%, , Hide
|
||||
}
|
||||
|
||||
WorkAreaOffset(left, top, right, bottom) {
|
||||
Run, komorebic.exe work-area-offset %left% %top% %right% %bottom%, , Hide
|
||||
}
|
||||
|
||||
AdjustContainerPadding(sizing, adjustment) {
|
||||
Run, komorebic.exe adjust-container-padding %sizing% %adjustment%, , Hide
|
||||
}
|
||||
@@ -100,8 +128,12 @@ AdjustWorkspacePadding(sizing, adjustment) {
|
||||
Run, komorebic.exe adjust-workspace-padding %sizing% %adjustment%, , Hide
|
||||
}
|
||||
|
||||
ChangeLayout(layout) {
|
||||
Run, komorebic.exe change-layout %layout%, , Hide
|
||||
ChangeLayout(default_layout) {
|
||||
Run, komorebic.exe change-layout %default_layout%, , Hide
|
||||
}
|
||||
|
||||
LoadCustomLayout(path) {
|
||||
Run, komorebic.exe load-custom-layout %path%, , Hide
|
||||
}
|
||||
|
||||
FlipLayout(flip) {
|
||||
@@ -132,6 +164,10 @@ WorkspaceLayout(monitor, workspace, value) {
|
||||
Run, komorebic.exe workspace-layout %monitor% %workspace% %value%, , Hide
|
||||
}
|
||||
|
||||
WorkspaceCustomLayout(monitor, workspace, path) {
|
||||
Run, komorebic.exe workspace-custom-layout %monitor% %workspace% %path%, , Hide
|
||||
}
|
||||
|
||||
WorkspaceTiling(monitor, workspace, value) {
|
||||
Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
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"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -17,7 +17,7 @@ komorebi-core = { path = "../komorebi-core" }
|
||||
|
||||
clap = "3.0.0-beta.4"
|
||||
color-eyre = "0.5"
|
||||
dirs = "3"
|
||||
dirs = "4"
|
||||
fs-tail = "0.1"
|
||||
heck = "0.3"
|
||||
paste = "1"
|
||||
|
||||
@@ -29,9 +29,9 @@ use derive_ahk::AhkFunction;
|
||||
use derive_ahk::AhkLibrary;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
@@ -79,10 +79,14 @@ macro_rules! gen_enum_subcommand_args {
|
||||
gen_enum_subcommand_args! {
|
||||
Focus: OperationDirection,
|
||||
Move: OperationDirection,
|
||||
CycleFocus: CycleDirection,
|
||||
CycleMove: CycleDirection,
|
||||
CycleMonitor: CycleDirection,
|
||||
CycleWorkspace: CycleDirection,
|
||||
Stack: OperationDirection,
|
||||
CycleStack: CycleDirection,
|
||||
FlipLayout: Flip,
|
||||
ChangeLayout: Layout,
|
||||
ChangeLayout: DefaultLayout,
|
||||
WatchConfiguration: BooleanState,
|
||||
Query: StateQuery,
|
||||
}
|
||||
@@ -128,7 +132,7 @@ macro_rules! gen_workspace_subcommand_args {
|
||||
$(#[clap(arg_enum)] $($arg_enum)?)?
|
||||
#[cfg_attr(
|
||||
all($(FALSE $($arg_enum)?)?),
|
||||
doc = ""$name" of the workspace as a "$value""
|
||||
doc = ""$name " of the workspace as a "$value ""
|
||||
)]
|
||||
value: $value,
|
||||
}
|
||||
@@ -139,10 +143,22 @@ macro_rules! gen_workspace_subcommand_args {
|
||||
|
||||
gen_workspace_subcommand_args! {
|
||||
Name: String,
|
||||
Layout: #[enum] Layout,
|
||||
Layout: #[enum] DefaultLayout,
|
||||
Tiling: #[enum] BooleanState,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
pub struct WorkspaceCustomLayout {
|
||||
/// Monitor index (zero-indexed)
|
||||
monitor: usize,
|
||||
|
||||
/// Workspace index on the specified monitor (zero-indexed)
|
||||
workspace: usize,
|
||||
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Resize {
|
||||
#[clap(arg_enum)]
|
||||
@@ -163,6 +179,18 @@ struct InvisibleBorders {
|
||||
bottom: i32,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct WorkAreaOffset {
|
||||
/// Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
left: i32,
|
||||
/// Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
top: i32,
|
||||
/// Size of the right work area offset
|
||||
right: i32,
|
||||
/// Size of the bottom work area offset
|
||||
bottom: i32,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct EnsureWorkspaces {
|
||||
/// Monitor index (zero-indexed)
|
||||
@@ -233,6 +261,7 @@ gen_application_target_subcommand_args! {
|
||||
ManageRule,
|
||||
IdentifyTrayApplication,
|
||||
IdentifyBorderOverflow,
|
||||
RemoveTitleBar,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
@@ -280,6 +309,24 @@ struct Load {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct LoadCustomLayout {
|
||||
/// JSON or YAML file from which the custom layout definition should be loaded
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Subscribe {
|
||||
/// Name of the pipe to send event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Unsubscribe {
|
||||
/// Name of the pipe to stop sending event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
|
||||
struct Opts {
|
||||
@@ -298,6 +345,12 @@ enum SubCommand {
|
||||
/// Query the current window manager state
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Query(Query),
|
||||
/// Subscribe to komorebi events
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Subscribe(Subscribe),
|
||||
/// Unsubscribe from komorebi events
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Unsubscribe(Unsubscribe),
|
||||
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
Log,
|
||||
/// Quicksave the current resize layout dimensions
|
||||
@@ -316,6 +369,12 @@ enum SubCommand {
|
||||
/// Move the focused window in the specified direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Move(Move),
|
||||
/// Change focus to the window in the specified cycle direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
CycleFocus(CycleFocus),
|
||||
/// Move the focused window in the specified cycle direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
CycleMove(CycleMove),
|
||||
/// Stack the focused window in the specified direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Stack(Stack),
|
||||
@@ -345,11 +404,20 @@ enum SubCommand {
|
||||
/// Focus the specified workspace on the focused monitor
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
FocusWorkspace(FocusWorkspace),
|
||||
/// Focus the monitor in the given cycle direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
CycleMonitor(CycleMonitor),
|
||||
/// Focus the workspace in the given cycle direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
CycleWorkspace(CycleWorkspace),
|
||||
/// 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),
|
||||
/// Set offsets to exclude parts of the work area from tiling
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkAreaOffset(WorkAreaOffset),
|
||||
/// Adjust container padding on the focused workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
AdjustContainerPadding(AdjustContainerPadding),
|
||||
@@ -359,6 +427,9 @@ enum SubCommand {
|
||||
/// Set the layout on the focused workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
ChangeLayout(ChangeLayout),
|
||||
/// Load a custom layout from file for the focused workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
LoadCustomLayout(LoadCustomLayout),
|
||||
/// Flip the layout on the focused workspace (BSP only)
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
FlipLayout(FlipLayout),
|
||||
@@ -378,6 +449,9 @@ enum SubCommand {
|
||||
/// Set the layout for the specified workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkspaceLayout(WorkspaceLayout),
|
||||
/// Set a custom layout for the specified workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkspaceCustomLayout(WorkspaceCustomLayout),
|
||||
/// Enable or disable window tiling for the specified workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkspaceTiling(WorkspaceTiling),
|
||||
@@ -420,6 +494,11 @@ enum SubCommand {
|
||||
/// Identify an application that has overflowing borders
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
IdentifyBorderOverflow(IdentifyBorderOverflow),
|
||||
/// Whitelist an application for title bar removal
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
RemoveTitleBar(RemoveTitleBar),
|
||||
/// Toggle title bars for whitelisted applications
|
||||
ToggleTitleBars,
|
||||
/// Enable or disable focus follows mouse for the operating system
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
FocusFollowsMouse(FocusFollowsMouse),
|
||||
@@ -493,6 +572,12 @@ fn main() -> Result<()> {
|
||||
SubCommand::Move(arg) => {
|
||||
send_message(&*SocketMessage::MoveWindow(arg.operation_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::CycleFocus(arg) => {
|
||||
send_message(&*SocketMessage::CycleFocusWindow(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::CycleMove(arg) => {
|
||||
send_message(&*SocketMessage::CycleMoveWindow(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::MoveToMonitor(arg) => {
|
||||
send_message(&*SocketMessage::MoveContainerToMonitorNumber(arg.target).as_bytes()?)?;
|
||||
}
|
||||
@@ -516,6 +601,17 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::WorkAreaOffset(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::WorkAreaOffset(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)
|
||||
@@ -559,6 +655,16 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::WorkspaceCustomLayout(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::WorkspaceLayoutCustom(
|
||||
arg.monitor,
|
||||
arg.workspace,
|
||||
resolve_windows_path(&arg.path)?,
|
||||
)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::WorkspaceTiling(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::WorkspaceTiling(arg.monitor, arg.workspace, arg.value.into())
|
||||
@@ -643,7 +749,12 @@ fn main() -> Result<()> {
|
||||
send_message(&*SocketMessage::CycleStack(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ChangeLayout(arg) => {
|
||||
send_message(&*SocketMessage::ChangeLayout(arg.layout).as_bytes()?)?;
|
||||
send_message(&*SocketMessage::ChangeLayout(arg.default_layout).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::LoadCustomLayout(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::ChangeLayoutCustom(resolve_windows_path(&arg.path)?).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::FlipLayout(arg) => {
|
||||
send_message(&*SocketMessage::FlipLayout(arg.flip).as_bytes()?)?;
|
||||
@@ -654,6 +765,12 @@ fn main() -> Result<()> {
|
||||
SubCommand::FocusWorkspace(arg) => {
|
||||
send_message(&*SocketMessage::FocusWorkspaceNumber(arg.target).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::CycleMonitor(arg) => {
|
||||
send_message(&*SocketMessage::CycleFocusMonitor(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::CycleWorkspace(arg) => {
|
||||
send_message(&*SocketMessage::CycleFocusWorkspace(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::NewWorkspace => {
|
||||
send_message(&*SocketMessage::NewWorkspace.as_bytes()?)?;
|
||||
}
|
||||
@@ -784,6 +901,23 @@ fn main() -> Result<()> {
|
||||
&*SocketMessage::IdentifyBorderOverflow(target.identifier, target.id).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::RemoveTitleBar(target) => {
|
||||
match target.identifier {
|
||||
ApplicationIdentifier::Exe => {}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"this command requires applications to be identified by their exe"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
send_message(
|
||||
&*SocketMessage::RemoveTitleBar(target.identifier, target.id).as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::ToggleTitleBars => {
|
||||
send_message(&*SocketMessage::ToggleTitleBars.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Manage => {
|
||||
send_message(&*SocketMessage::ManageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
@@ -802,6 +936,12 @@ fn main() -> Result<()> {
|
||||
SubCommand::Load(arg) => {
|
||||
send_message(&*SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Subscribe(arg) => {
|
||||
send_message(&*SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Unsubscribe(arg) => {
|
||||
send_message(&*SocketMessage::RemoveSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
imports_granularity = "Item"
|
||||
Reference in New Issue
Block a user