mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-15 17:57:40 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80dd07fcde | ||
|
|
09d1d69668 | ||
|
|
786f5e846a | ||
|
|
65bc1a966e | ||
|
|
ddafe599a2 | ||
|
|
7ed6df511f | ||
|
|
f9c4dbd447 | ||
|
|
b344888b72 | ||
|
|
a62ed682de | ||
|
|
94e9bb8e9e | ||
|
|
644f7ee604 | ||
|
|
b9a40924a8 | ||
|
|
80bcb51f75 | ||
|
|
e10e11d1de | ||
|
|
2807cafdd0 | ||
|
|
63cf48daa5 |
107
Cargo.lock
generated
107
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",
|
||||
]
|
||||
@@ -425,9 +425,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 +471,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 +505,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebi"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"bitflags",
|
||||
@@ -537,7 +537,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
@@ -549,7 +549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebic"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
@@ -580,9 +580,9 @@ 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 = "lock_api"
|
||||
@@ -701,9 +701,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 +853,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 +883,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",
|
||||
]
|
||||
@@ -1113,24 +1113,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "740223c51853f3145fe7c90360d2d4232f2b62e3449489c207eccde818979982"
|
||||
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 +1161,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 +1172,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 +1245,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.27"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2ba9ab62b7d6497a8638dfda5e5c4fb3b2d5a7fca4118f2b96151c8ef1a437e"
|
||||
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"pin-project-lite",
|
||||
@@ -1268,9 +1268,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 +1279,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 +1319,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.2.23"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c42e73a9d277d4d2b6a88389a137ccf3c58599660b17e8f5fc39305e490669"
|
||||
checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"chrono",
|
||||
@@ -1452,20 +1452,21 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.19.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef84dd25f4c69a271b1bba394532bf400523b43169de21dfc715e8f8e491053d"
|
||||
checksum = "b8f5f8d2ea79bf690bbee453fd4a1516ae426e5d5c7215d96cc0c3dc134fc4a0"
|
||||
dependencies = [
|
||||
"const-sha1",
|
||||
"windows_gen",
|
||||
"windows_macros",
|
||||
"windows_reader",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_gen"
|
||||
version = "0.19.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac7bb21b8ff5e801232b72a6ff554b4cc0cef9ed9238188c3ca78fe3968a7e5d"
|
||||
checksum = "7e6994f42f8481387778cc608407d6703410672d57f32a66009419d7a18aa912"
|
||||
dependencies = [
|
||||
"windows_quote",
|
||||
"windows_reader",
|
||||
@@ -1473,9 +1474,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_macros"
|
||||
version = "0.19.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5566b8c51118769e4a9094a688bf1233a3f36aacbfc78f3b15817fe0b6e0442f"
|
||||
checksum = "81cc2357b1b03c19f056cb0e6d06011f80f54beadb4e36aee2ca98493c7cfc3c"
|
||||
dependencies = [
|
||||
"syn",
|
||||
"windows_gen",
|
||||
@@ -1485,15 +1486,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_quote"
|
||||
version = "0.19.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4af8236a9493c38855f95cdd11b38b342512a5df4ee7473cffa828b5ebb0e39c"
|
||||
checksum = "7cf987b5288c15e1997226848f78f3ed3ef8b78dcfd71a201c8c8684163a7e4d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_reader"
|
||||
version = "0.19.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c8d5cf83fb08083438c5c46723e6206b2970da57ce314f80b57724439aaacab"
|
||||
checksum = "237b53e8b40766ea7db5da0d8c6c1442d21d0429f0ee7500d7b5688967bd9d7b"
|
||||
|
||||
[[package]]
|
||||
name = "winput"
|
||||
|
||||
44
README.md
44
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
|
||||
@@ -184,6 +188,26 @@ passing it as an argument to the `--implementation` flag:
|
||||
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
|
||||
```
|
||||
|
||||
#### Saving and Loading Resized Layouts
|
||||
|
||||
If you create a BSP layout through various resize adjustments that you want to be able to restore easily in the future,
|
||||
it is possible to "quicksave" that layout to the system's temporary folder and load it later in the same session, or
|
||||
alternatively, you may save it to a specific file to be loaded again at any point in the future.
|
||||
|
||||
```powershell
|
||||
komorebic.exe quick-save # saves the focused workspace to $Env:TEMP\komorebi.quicksave.json
|
||||
komorebic.exe quick-load # loads $Env:TEMP\komorebi.quicksave.json on the focused workspace
|
||||
|
||||
komorebic.exe save ~/layouts/primary.json # saves the focused workspace to $Env:USERPROFILE\layouts\primary.json
|
||||
komorebic.exe load ~/layouts/secondary.json # loads $Env:USERPROFILE\layouts\secondary.json on the focused workspace
|
||||
```
|
||||
|
||||
These layouts can be applied to arbitrary collections of windows on any workspace, as they only track the layout
|
||||
dimensions and are not coupled to the applications that were running at the time of saving.
|
||||
|
||||
When layouts that expect more or less windows than the number currently on the focused workspace are loaded, `komorebi`
|
||||
will automatically reconcile the difference.
|
||||
|
||||
## Configuration with `komorebic`
|
||||
|
||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||
@@ -200,8 +224,14 @@ stop Stop the komorebi.exe process and restore all hidd
|
||||
state Show a JSON representation of the current window manager state
|
||||
query Query the current window manager state
|
||||
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
quick-save Quicksave the current resize layout dimensions
|
||||
quick-load Load the last quicksaved resize layout dimensions
|
||||
save Save the current resize layout dimensions to a file
|
||||
load Load the resize layout dimensions from a file
|
||||
focus Change focus to the window in the specified direction
|
||||
move Move the focused window in the specified direction
|
||||
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
|
||||
@@ -212,8 +242,11 @@ 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
|
||||
@@ -272,17 +305,24 @@ used [is available here](komorebi.sample.with.lib.ahk).
|
||||
- [x] Mouse follows focused container
|
||||
- [x] Resize window container in direction
|
||||
- [ ] Resize child window containers by split ratio
|
||||
- [x] Quicksave and quickload layouts with resize dimensions
|
||||
- [x] Save and load layouts with resize dimensions to/from specific files
|
||||
- [x] Mouse drag to swap window container position
|
||||
- [x] Mouse drag to resize window container
|
||||
- [x] Configurable workspace and container gaps
|
||||
- [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] 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
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
windows = "0.19"
|
||||
windows = "0.21"
|
||||
|
||||
[build-dependencies]
|
||||
windows = "0.19"
|
||||
windows = "0.21"
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
pub use windows::Handle;
|
||||
pub use windows::Result;
|
||||
|
||||
::windows::include_bindings!();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,9 @@ pub enum Layout {
|
||||
BSP,
|
||||
Columns,
|
||||
Rows,
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
@@ -131,7 +134,11 @@ impl Layout {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::too_many_lines
|
||||
)]
|
||||
pub fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
@@ -185,6 +192,176 @@ impl Layout {
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::VerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut iter = layouts.iter_mut();
|
||||
{
|
||||
if let Some(first) = iter.next() {
|
||||
first.left = main_left;
|
||||
first.top = area.top;
|
||||
first.right = primary_right;
|
||||
first.bottom = area.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
let bottom = area.bottom / (len - 1) as i32;
|
||||
let mut top = 0;
|
||||
|
||||
for next in iter {
|
||||
next.left = stack_left;
|
||||
next.top = area.top + top;
|
||||
next.right = area.right - primary_right;
|
||||
next.bottom = bottom;
|
||||
|
||||
top += bottom;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::HorizontalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut iter = layouts.iter_mut();
|
||||
{
|
||||
if let Some(first) = iter.next() {
|
||||
first.left = area.left;
|
||||
first.top = main_top;
|
||||
first.right = area.right;
|
||||
first.bottom = bottom;
|
||||
}
|
||||
}
|
||||
|
||||
let right = area.right / (len - 1) as i32;
|
||||
let mut left = 0;
|
||||
|
||||
for next in iter {
|
||||
next.left = area.left + left;
|
||||
next.top = stack_top;
|
||||
next.right = right;
|
||||
next.bottom = area.bottom - bottom;
|
||||
|
||||
left += right;
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Layout::UltrawideVerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
let mut iter = layouts.iter_mut();
|
||||
|
||||
{
|
||||
if let Some(first) = iter.next() {
|
||||
first.left = primary_left;
|
||||
first.top = area.top;
|
||||
first.right = primary_right;
|
||||
first.bottom = area.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(second) = iter.next() {
|
||||
second.left = secondary_left;
|
||||
second.top = area.top;
|
||||
second.right = secondary_right;
|
||||
second.bottom = area.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
if len > 2 {
|
||||
let height = area.bottom / (len - 2) as i32;
|
||||
let mut y = 0;
|
||||
|
||||
for next in iter {
|
||||
next.left = stack_left;
|
||||
next.top = area.top + y;
|
||||
next.right = secondary_right;
|
||||
next.bottom = height;
|
||||
y += height;
|
||||
}
|
||||
}
|
||||
layouts
|
||||
}
|
||||
};
|
||||
|
||||
dimensions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ArgEnum;
|
||||
@@ -26,6 +27,8 @@ pub enum SocketMessage {
|
||||
// Window / Container Commands
|
||||
FocusWindow(OperationDirection),
|
||||
MoveWindow(OperationDirection),
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
ResizeWindow(OperationDirection, Sizing),
|
||||
UnstackWindow,
|
||||
@@ -52,6 +55,12 @@ pub enum SocketMessage {
|
||||
Stop,
|
||||
TogglePause,
|
||||
Retile,
|
||||
QuickSave,
|
||||
QuickLoad,
|
||||
Save(PathBuf),
|
||||
Load(PathBuf),
|
||||
CycleFocusMonitor(CycleDirection),
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusWorkspaceNumber(usize),
|
||||
ContainerPadding(usize, usize, i32),
|
||||
@@ -63,6 +72,7 @@ pub enum SocketMessage {
|
||||
ReloadConfiguration,
|
||||
WatchConfiguration(bool),
|
||||
InvisibleBorders(Rect),
|
||||
WorkAreaOffset(Rect),
|
||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
|
||||
@@ -57,25 +57,39 @@ impl OperationDirection {
|
||||
len: usize,
|
||||
) -> bool {
|
||||
match Self::flip_direction(self, layout_flip) {
|
||||
OperationDirection::Up => match layout {
|
||||
Self::Up => match layout {
|
||||
Layout::BSP => len > 2 && idx != 0 && idx != 1,
|
||||
Layout::Columns => false,
|
||||
Layout::Rows => idx != 0,
|
||||
Layout::Rows | Layout::HorizontalStack => idx != 0,
|
||||
Layout::VerticalStack => idx != 0 && idx != 1,
|
||||
Layout::UltrawideVerticalStack => idx > 2,
|
||||
},
|
||||
OperationDirection::Down => match layout {
|
||||
Self::Down => match layout {
|
||||
Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0,
|
||||
Layout::Columns => false,
|
||||
Layout::Rows => idx != len - 1,
|
||||
Layout::VerticalStack => idx != 0 && idx != len - 1,
|
||||
Layout::HorizontalStack => idx == 0,
|
||||
Layout::UltrawideVerticalStack => idx > 1 && idx != len - 1,
|
||||
},
|
||||
OperationDirection::Left => match layout {
|
||||
Self::Left => match layout {
|
||||
Layout::BSP => len > 1 && idx != 0,
|
||||
Layout::Columns => idx != 0,
|
||||
Layout::Columns | Layout::VerticalStack => idx != 0,
|
||||
Layout::Rows => false,
|
||||
Layout::HorizontalStack => idx != 0 && idx != 1,
|
||||
Layout::UltrawideVerticalStack => len > 1 && idx != 1,
|
||||
},
|
||||
OperationDirection::Right => match layout {
|
||||
Self::Right => match layout {
|
||||
Layout::BSP => len > 1 && idx % 2 == 0 && idx != len - 1,
|
||||
Layout::Columns => idx != len - 1,
|
||||
Layout::Rows => false,
|
||||
Layout::VerticalStack => idx == 0,
|
||||
Layout::HorizontalStack => idx != 0 && idx != len - 1,
|
||||
Layout::UltrawideVerticalStack => match len {
|
||||
0 | 1 => false,
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -92,11 +106,16 @@ impl OperationDirection {
|
||||
}
|
||||
}
|
||||
Layout::Columns => unreachable!(),
|
||||
Layout::Rows => idx - 1,
|
||||
Layout::Rows | Layout::VerticalStack | Layout::UltrawideVerticalStack => idx - 1,
|
||||
Layout::HorizontalStack => 0,
|
||||
},
|
||||
Self::Down => match layout {
|
||||
Layout::BSP | Layout::Rows => idx + 1,
|
||||
Layout::BSP
|
||||
| Layout::Rows
|
||||
| Layout::VerticalStack
|
||||
| Layout::UltrawideVerticalStack => idx + 1,
|
||||
Layout::Columns => unreachable!(),
|
||||
Layout::HorizontalStack => 1,
|
||||
},
|
||||
Self::Left => match layout {
|
||||
Layout::BSP => {
|
||||
@@ -106,12 +125,24 @@ impl OperationDirection {
|
||||
idx - 1
|
||||
}
|
||||
}
|
||||
Layout::Columns => idx - 1,
|
||||
Layout::Columns | Layout::HorizontalStack => idx - 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
Layout::VerticalStack => 0,
|
||||
Layout::UltrawideVerticalStack => match idx {
|
||||
0 => 1,
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
},
|
||||
Self::Right => match layout {
|
||||
Layout::BSP | Layout::Columns => idx + 1,
|
||||
Layout::BSP | Layout::Columns | Layout::HorizontalStack => idx + 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
Layout::VerticalStack => 1,
|
||||
Layout::UltrawideVerticalStack => match idx {
|
||||
1 => 0,
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,16 +40,24 @@ FloatRule("exe", "Wally.exe")
|
||||
FloatRule("exe", "wincompose.exe")
|
||||
FloatRule("exe", "1Password.exe")
|
||||
FloatRule("exe", "Wox.exe")
|
||||
FloatRule("exe", "ddm.exe")
|
||||
FloatRule("class", "Chrome_RenderWidgetHostHWND") ; GOG Electron invisible overlay
|
||||
FloatRule("class", "CEFCLIENT")
|
||||
|
||||
; Identify Minimize-to-Tray Applications
|
||||
IdentifyTrayApplication("exe", "Discord.exe")
|
||||
IdentifyTrayApplication("exe", "Spotify.exe")
|
||||
IdentifyTrayApplication("exe", "GalaxyClient.exe")
|
||||
|
||||
; Identify Electron applications with overflowing borders
|
||||
IdentifyBorderOverflow("exe", "Discord.exe")
|
||||
IdentifyBorderOverflow("exe", "Spotify.exe")
|
||||
IdentifyBorderOverflow("exe", "GalaxyClient.exe")
|
||||
IdentifyBorderOverflow("class", "ZPFTEWndClass")
|
||||
|
||||
; Identify applications to be forcibly managed
|
||||
ManageRule("exe", "GalaxyClient.exe")
|
||||
|
||||
; Change the focused window, Alt + Vim direction keys
|
||||
!h::
|
||||
Focus("left")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::fs::File;
|
||||
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;
|
||||
@@ -12,6 +15,7 @@ use parking_lot::Mutex;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::StateQuery;
|
||||
|
||||
@@ -63,6 +67,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) => {
|
||||
@@ -127,6 +137,16 @@ 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)?;
|
||||
@@ -140,6 +160,31 @@ impl WindowManager {
|
||||
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
|
||||
self.set_workspace_layout(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
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
@@ -326,6 +371,66 @@ 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();
|
||||
|
||||
let mut quicksave_json = std::env::temp_dir();
|
||||
quicksave_json.push("komorebi.quicksave.json");
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(quicksave_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &resize)?;
|
||||
}
|
||||
SocketMessage::QuickLoad => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let mut quicksave_json = std::env::temp_dir();
|
||||
quicksave_json.push("komorebi.quicksave.json");
|
||||
|
||||
let file = File::open(&quicksave_json).map_err(|_| {
|
||||
anyhow!(
|
||||
"no quicksave found at {}",
|
||||
quicksave_json.display().to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
SocketMessage::Save(path) => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let resize = workspace.resize_dimensions();
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(path)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &resize)?;
|
||||
}
|
||||
SocketMessage::Load(path) => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let file = File::open(&path)
|
||||
.map_err(|_| anyhow!("no file found at {}", path.display().to_string()))?;
|
||||
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("processed");
|
||||
|
||||
@@ -66,13 +66,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,
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
@@ -46,6 +45,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>,
|
||||
@@ -57,6 +57,7 @@ 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 float_identifiers: Vec<String>,
|
||||
@@ -73,6 +74,7 @@ impl From<&mut WindowManager> for State {
|
||||
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,
|
||||
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
|
||||
@@ -144,6 +146,7 @@ impl WindowManager {
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
},
|
||||
work_area_offset: None,
|
||||
focus_follows_mouse: None,
|
||||
hotwatch: Hotwatch::new()?,
|
||||
virtual_desktop_id,
|
||||
@@ -266,6 +269,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;
|
||||
@@ -294,7 +298,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if should_update {
|
||||
monitor.update_focused_workspace(&invisible_borders)?;
|
||||
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +428,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
|
||||
@@ -435,7 +441,7 @@ impl WindowManager {
|
||||
*resize = None;
|
||||
}
|
||||
|
||||
workspace.update(&work_area, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -535,10 +541,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() {
|
||||
@@ -591,9 +598,9 @@ impl WindowManager {
|
||||
) {
|
||||
let unaltered = workspace.layout().calculate(
|
||||
&work_area,
|
||||
NonZeroUsize::new(len).context(
|
||||
"there must be at least one container to calculate a workspace layout",
|
||||
)?,
|
||||
NonZeroUsize::new(len).ok_or_else(|| {
|
||||
anyhow!("there must be at least one container to calculate a workspace layout")
|
||||
})?,
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
@@ -661,6 +668,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()
|
||||
@@ -686,7 +694,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)?;
|
||||
@@ -740,18 +748,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();
|
||||
@@ -1042,6 +1084,7 @@ impl WindowManager {
|
||||
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
|
||||
@@ -1061,7 +1104,7 @@ impl WindowManager {
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
workspace.update(&work_area, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(self.update_focused_workspace(false)?)
|
||||
|
||||
@@ -7,6 +7,8 @@ use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::Error;
|
||||
use color_eyre::Result;
|
||||
|
||||
use bindings::Handle;
|
||||
use bindings::Result as WindowsCrateResult;
|
||||
use bindings::Windows::Win32::Foundation::BOOL;
|
||||
use bindings::Windows::Win32::Foundation::HANDLE;
|
||||
use bindings::Windows::Win32::Foundation::HWND;
|
||||
@@ -92,36 +94,6 @@ pub enum WindowsResult<T, E> {
|
||||
Ok(T),
|
||||
}
|
||||
|
||||
impl From<BOOL> for WindowsResult<(), Error> {
|
||||
fn from(return_value: BOOL) -> Self {
|
||||
if return_value.as_bool() {
|
||||
Self::Ok(())
|
||||
} else {
|
||||
Self::Err(std::io::Error::last_os_error().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HWND> for WindowsResult<isize, Error> {
|
||||
fn from(return_value: HWND) -> Self {
|
||||
if return_value.is_null() {
|
||||
Self::Err(std::io::Error::last_os_error().into())
|
||||
} else {
|
||||
Self::Ok(return_value.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HANDLE> for WindowsResult<HANDLE, Error> {
|
||||
fn from(return_value: HANDLE) -> Self {
|
||||
if return_value.is_null() {
|
||||
Self::Err(std::io::Error::last_os_error().into())
|
||||
} else {
|
||||
Self::Ok(return_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_integer_for_windows_result {
|
||||
( $( $integer_type:ty ),+ ) => {
|
||||
$(
|
||||
@@ -148,6 +120,40 @@ impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ProcessWindowsCrateResult<T> {
|
||||
fn process(self) -> Result<T>;
|
||||
}
|
||||
|
||||
macro_rules! impl_process_windows_crate_result {
|
||||
( $($input:ty => $deref:ty),+ $(,)? ) => (
|
||||
paste::paste! {
|
||||
$(
|
||||
impl ProcessWindowsCrateResult<$deref> for WindowsCrateResult<$input> {
|
||||
fn process(self) -> Result<$deref> {
|
||||
match self {
|
||||
Ok(value) => Ok(value.0),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
impl_process_windows_crate_result!(
|
||||
HWND => isize,
|
||||
);
|
||||
|
||||
impl<T> ProcessWindowsCrateResult<T> for WindowsCrateResult<T> {
|
||||
fn process(self) -> Result<T> {
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowsApi;
|
||||
|
||||
impl WindowsApi {
|
||||
@@ -155,14 +161,16 @@ impl WindowsApi {
|
||||
callback: MONITORENUMPROC,
|
||||
callback_data_address: isize,
|
||||
) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
unsafe {
|
||||
EnumDisplayMonitors(
|
||||
HDC(0),
|
||||
std::ptr::null_mut(),
|
||||
Option::from(callback),
|
||||
LPARAM(callback_data_address),
|
||||
)
|
||||
}))
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn valid_hmonitors() -> Result<Vec<isize>> {
|
||||
@@ -184,9 +192,9 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
EnumWindows(Option::from(callback), LPARAM(callback_data_address))
|
||||
}))
|
||||
unsafe { EnumWindows(Option::from(callback), LPARAM(callback_data_address)) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||
@@ -225,9 +233,9 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
AllowSetForegroundWindow(process_id)
|
||||
}))
|
||||
unsafe { AllowSetForegroundWindow(process_id) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn monitor_from_window(hwnd: HWND) -> isize {
|
||||
@@ -250,7 +258,7 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
position,
|
||||
@@ -260,7 +268,9 @@ impl WindowsApi {
|
||||
layout.bottom,
|
||||
SET_WINDOW_POS_FLAGS(flags),
|
||||
)
|
||||
}))
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||
@@ -282,39 +292,25 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn foreground_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetForegroundWindow() }))
|
||||
unsafe { GetForegroundWindow() }.ok().process()
|
||||
}
|
||||
|
||||
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
|
||||
match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) {
|
||||
WindowsResult::Ok(_) => Ok(()),
|
||||
WindowsResult::Err(error) => {
|
||||
// TODO: Figure out the odd behaviour here, docs state that a zero value means
|
||||
// TODO: that the window was not brought to the foreground, but this contradicts
|
||||
// TODO: the behaviour that I have observed which resulted in this check
|
||||
if error.to_string() == "The operation completed successfully. (os error 0)" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { SetForegroundWindow(hwnd) }.ok().process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn top_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetTopWindow(HWND::NULL).0 }))
|
||||
unsafe { GetTopWindow(HWND::default()) }.ok().process()
|
||||
}
|
||||
|
||||
pub fn desktop_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetDesktopWindow() }))
|
||||
unsafe { GetDesktopWindow() }.ok().process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn next_window(hwnd: HWND) -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindow(hwnd, GW_HWNDNEXT).0
|
||||
}))
|
||||
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok().process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -335,30 +331,24 @@ impl WindowsApi {
|
||||
|
||||
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
||||
let mut rect = unsafe { std::mem::zeroed() };
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowRect(hwnd, &mut rect)
|
||||
}))?;
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.ok().process()?;
|
||||
|
||||
Ok(Rect::from(rect))
|
||||
}
|
||||
|
||||
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe { SetCursorPos(x, y) }))
|
||||
unsafe { SetCursorPos(x, y) }.ok().process()
|
||||
}
|
||||
|
||||
pub fn cursor_pos() -> Result<POINT> {
|
||||
let mut cursor_pos: POINT = unsafe { std::mem::zeroed() };
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetCursorPos(&mut cursor_pos)
|
||||
}))?;
|
||||
let mut cursor_pos = POINT::default();
|
||||
unsafe { GetCursorPos(&mut cursor_pos) }.ok().process()?;
|
||||
|
||||
Ok(cursor_pos)
|
||||
}
|
||||
|
||||
pub fn window_from_point(point: POINT) -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { WindowFromPoint(point) }))
|
||||
unsafe { WindowFromPoint(point) }.ok().process()
|
||||
}
|
||||
|
||||
pub fn window_at_cursor_pos() -> Result<isize> {
|
||||
@@ -388,24 +378,13 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
AttachThreadInput(thread_id, target_thread_id, attach)
|
||||
}))
|
||||
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn set_focus(hwnd: HWND) -> Result<()> {
|
||||
match WindowsResult::from(unsafe { SetFocus(hwnd) }) {
|
||||
WindowsResult::Ok(_) => Ok(()),
|
||||
WindowsResult::Err(error) => {
|
||||
// If the window is not attached to the calling thread's message queue, the return value is NULL
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setfocus
|
||||
if error.to_string() == "The operation completed successfully. (os error 0)" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { SetFocus(hwnd) }.ok().map(|_| ()).process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -457,9 +436,9 @@ impl WindowsApi {
|
||||
inherit_handle: bool,
|
||||
process_id: u32,
|
||||
) -> Result<HANDLE> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
OpenProcess(access_rights, inherit_handle, process_id)
|
||||
}))
|
||||
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
|
||||
@@ -471,14 +450,16 @@ impl WindowsApi {
|
||||
let mut path: Vec<u16> = vec![0; len as usize];
|
||||
let text_ptr = path.as_mut_ptr();
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
unsafe {
|
||||
QueryFullProcessImageNameW(
|
||||
handle,
|
||||
PROCESS_NAME_FORMAT(0),
|
||||
PWSTR(text_ptr),
|
||||
&mut len as *mut u32,
|
||||
)
|
||||
}))?;
|
||||
}
|
||||
.ok()
|
||||
.process()?;
|
||||
|
||||
Ok(String::from_utf16(&path[..len as usize])?)
|
||||
}
|
||||
@@ -553,9 +534,9 @@ impl WindowsApi {
|
||||
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
|
||||
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
|
||||
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast())
|
||||
}))?;
|
||||
unsafe { GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast()) }
|
||||
.ok()
|
||||
.process()?;
|
||||
|
||||
Ok(monitor_info)
|
||||
}
|
||||
@@ -577,9 +558,9 @@ impl WindowsApi {
|
||||
pv_param: *mut c_void,
|
||||
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
|
||||
) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
SystemParametersInfoW(action, ui_param, pv_param, update_flags)
|
||||
}))
|
||||
unsafe { SystemParametersInfoW(action, ui_param, pv_param, update_flags) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::collections::VecDeque;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
@@ -10,6 +9,7 @@ use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
@@ -48,8 +48,7 @@ pub struct Workspace {
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
latest_layout: Vec<Rect>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
resize_dimensions: Vec<Option<Rect>>,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
tile: bool,
|
||||
@@ -139,8 +138,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();
|
||||
@@ -148,6 +165,7 @@ 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() {
|
||||
@@ -155,9 +173,11 @@ impl Workspace {
|
||||
} else if !self.containers().is_empty() {
|
||||
let layouts = self.layout().calculate(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).context(
|
||||
"there must be at least one container to calculate a workspace layout",
|
||||
)?,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"there must be at least one container to calculate a workspace layout"
|
||||
)
|
||||
})?,
|
||||
self.container_padding(),
|
||||
self.layout_flip(),
|
||||
self.resize_dimensions(),
|
||||
@@ -324,11 +344,14 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn promote_container(&mut self) -> Result<()> {
|
||||
let resize = self.resize_dimensions_mut().remove(0);
|
||||
let container = self
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
self.containers_mut().push_front(container);
|
||||
self.resize_dimensions_mut().insert(0, None);
|
||||
self.resize_dimensions_mut().insert(0, resize);
|
||||
|
||||
self.focus_container(0);
|
||||
|
||||
Ok(())
|
||||
@@ -340,8 +363,15 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||
self.resize_dimensions_mut().remove(idx);
|
||||
self.containers_mut().remove(idx)
|
||||
if idx < self.resize_dimensions().len() {
|
||||
self.resize_dimensions_mut().remove(idx);
|
||||
}
|
||||
|
||||
if idx < self.containers().len() {
|
||||
return self.containers_mut().remove(idx);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {
|
||||
@@ -447,6 +477,12 @@ impl Workspace {
|
||||
None
|
||||
}
|
||||
}
|
||||
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<()> {
|
||||
let focused_idx = self.focused_container_idx();
|
||||
|
||||
@@ -20,6 +20,22 @@ Log() {
|
||||
Run, komorebic.exe log, , Hide
|
||||
}
|
||||
|
||||
QuickSave() {
|
||||
Run, komorebic.exe quick-save, , Hide
|
||||
}
|
||||
|
||||
QuickLoad() {
|
||||
Run, komorebic.exe quick-load, , Hide
|
||||
}
|
||||
|
||||
Save(path) {
|
||||
Run, komorebic.exe save %path%, , Hide
|
||||
}
|
||||
|
||||
Load(path) {
|
||||
Run, komorebic.exe load %path%, , Hide
|
||||
}
|
||||
|
||||
Focus(operation_direction) {
|
||||
Run, komorebic.exe focus %operation_direction%, , Hide
|
||||
}
|
||||
@@ -28,6 +44,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
|
||||
}
|
||||
@@ -68,6 +92,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
|
||||
}
|
||||
@@ -76,6 +108,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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.4"
|
||||
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"]
|
||||
@@ -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"
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::process::Command;
|
||||
use clap::AppSettings;
|
||||
use clap::ArgEnum;
|
||||
use clap::Clap;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use fs_tail::TailedFile;
|
||||
use heck::KebabCase;
|
||||
@@ -79,6 +79,10 @@ 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,
|
||||
@@ -163,6 +167,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)
|
||||
@@ -268,6 +284,18 @@ struct Start {
|
||||
ffm: bool,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Save {
|
||||
/// File to which the resize layout dimensions should be saved
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap, AhkFunction)]
|
||||
struct Load {
|
||||
/// File from which the resize layout dimensions should be loaded
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
|
||||
struct Opts {
|
||||
@@ -288,12 +316,28 @@ enum SubCommand {
|
||||
Query(Query),
|
||||
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
Log,
|
||||
/// Quicksave the current resize layout dimensions
|
||||
QuickSave,
|
||||
/// Load the last quicksaved resize layout dimensions
|
||||
QuickLoad,
|
||||
/// Save the current resize layout dimensions to a file
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Save(Save),
|
||||
/// Load the resize layout dimensions from a file
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Load(Load),
|
||||
/// Change focus to the window in the specified direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Focus(Focus),
|
||||
/// 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),
|
||||
@@ -323,11 +367,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),
|
||||
@@ -409,7 +462,7 @@ enum SubCommand {
|
||||
}
|
||||
|
||||
pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
let mut socket = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut socket = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
socket.push("komorebi.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
@@ -423,7 +476,8 @@ fn main() -> Result<()> {
|
||||
|
||||
match opts.subcmd {
|
||||
SubCommand::AhkLibrary => {
|
||||
let mut library = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut library =
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
library.push("komorebic.lib.ahk");
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -435,9 +489,9 @@ fn main() -> Result<()> {
|
||||
|
||||
println!(
|
||||
"\nAHK helper library for komorebic written to {}",
|
||||
library
|
||||
.to_str()
|
||||
.context("could not find the path to the generated ahk lib file")?
|
||||
library.to_str().ok_or_else(|| anyhow!(
|
||||
"could not find the path to the generated ahk lib file"
|
||||
))?
|
||||
);
|
||||
|
||||
println!(
|
||||
@@ -470,6 +524,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()?)?;
|
||||
}
|
||||
@@ -493,6 +553,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)
|
||||
@@ -555,10 +626,9 @@ fn main() -> Result<()> {
|
||||
buf.pop(); // %USERPROFILE%\scoop\shims
|
||||
buf.pop(); // %USERPROFILE%\scoop
|
||||
buf.push("apps\\komorebi\\current\\komorebi.exe"); //%USERPROFILE%\scoop\komorebi\current\komorebi.exe
|
||||
Option::from(
|
||||
buf.to_str()
|
||||
.context("cannot create a string from the scoop komorebi path")?,
|
||||
)
|
||||
Option::from(buf.to_str().ok_or_else(|| {
|
||||
anyhow!("cannot create a string from the scoop komorebi path")
|
||||
})?)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -632,6 +702,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()?)?;
|
||||
}
|
||||
@@ -648,7 +724,7 @@ fn main() -> Result<()> {
|
||||
)?;
|
||||
}
|
||||
SubCommand::State => {
|
||||
let home = dirs::home_dir().context("there is no home directory")?;
|
||||
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
@@ -682,7 +758,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
SubCommand::Query(arg) => {
|
||||
let home = dirs::home_dir().context("there is no home directory")?;
|
||||
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
let mut socket = home;
|
||||
socket.push("komorebic.sock");
|
||||
let socket = socket.as_path();
|
||||
@@ -716,7 +792,8 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
SubCommand::RestoreWindows => {
|
||||
let mut hwnd_json = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut hwnd_json =
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
hwnd_json.push("komorebi.hwnd.json");
|
||||
|
||||
let file = File::open(hwnd_json)?;
|
||||
@@ -767,11 +844,54 @@ fn main() -> Result<()> {
|
||||
SubCommand::Unmanage => {
|
||||
send_message(&*SocketMessage::UnmanageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::QuickSave => {
|
||||
send_message(&*SocketMessage::QuickSave.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::QuickLoad => {
|
||||
send_message(&*SocketMessage::QuickLoad.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Save(arg) => {
|
||||
send_message(&*SocketMessage::Save(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Load(arg) => {
|
||||
send_message(&*SocketMessage::Load(resolve_windows_path(&arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_windows_path(raw_path: &str) -> Result<PathBuf> {
|
||||
let path = if raw_path.starts_with('~') {
|
||||
raw_path.replacen(
|
||||
"~",
|
||||
&dirs::home_dir()
|
||||
.ok_or_else(|| anyhow!("there is no home directory"))?
|
||||
.display()
|
||||
.to_string(),
|
||||
1,
|
||||
)
|
||||
} else {
|
||||
raw_path.to_string()
|
||||
};
|
||||
|
||||
let full_path = PathBuf::from(path);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot parse directory"))?;
|
||||
|
||||
let file = full_path
|
||||
.components()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("cannot parse filename"))?;
|
||||
|
||||
let mut canonicalized = std::fs::canonicalize(parent)?;
|
||||
canonicalized.push(file);
|
||||
|
||||
Ok(canonicalized)
|
||||
}
|
||||
|
||||
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||
// BOOL is returned but does not signify whether or not the operation was succesful
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
|
||||
|
||||
Reference in New Issue
Block a user