mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-14 22:13:13 +01:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
381253da20 | ||
|
|
cf7532330b | ||
|
|
ffb86458f0 | ||
|
|
292bdb282f | ||
|
|
1625ca6e5d | ||
|
|
df07409a2f | ||
|
|
2e86b607b2 | ||
|
|
6f7e87799b | ||
|
|
4e9b294835 | ||
|
|
8ffe6f78b7 | ||
|
|
42b9305dfe | ||
|
|
1eba8aa01d | ||
|
|
74811fbe13 | ||
|
|
209cd82892 | ||
|
|
98f731ba13 | ||
|
|
c7bf09e34b | ||
|
|
0725549d45 | ||
|
|
13b335cecc | ||
|
|
23aada05d0 | ||
|
|
f11dcbc0cb | ||
|
|
564ee89c84 |
21
.github/dependabot.yml
vendored
Normal file
21
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
assignees:
|
||||
- "LGUG2Z"
|
||||
commit-message:
|
||||
prefix: chore
|
||||
include: scope
|
||||
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "LGUG2Z"
|
||||
commit-message:
|
||||
prefix: chore
|
||||
include: scope
|
||||
2
.github/workflows/windows.yaml
vendored
2
.github/workflows/windows.yaml
vendored
@@ -20,6 +20,8 @@ jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -52,6 +52,8 @@ scoop:
|
||||
homepage: https://github.com/LGUG2Z/komorebi
|
||||
description: A tiling window manager for Windows
|
||||
license: MIT
|
||||
pre_install:
|
||||
- if (Get-Process -Name komorebi -ErrorAction SilentlyContinue) { komorebic stop }
|
||||
post_install:
|
||||
- Write-Host "Run 'cp $original_dir\komorebi.sample.ahk $Env:UserProfile\komorebi.ahk' to get started with the sample configuration"
|
||||
- Write-Host "Once you have a configuration file in place, you can run 'komorebic start' to start the window manager"
|
||||
|
||||
163
Cargo.lock
generated
163
Cargo.lock
generated
@@ -160,6 +160,37 @@ dependencies = [
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "com"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a30a2b2a013da986dc5cc3eda3d19c0d59d53f835be1b2356eb8d00f000c793"
|
||||
dependencies = [
|
||||
"com_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "com_macros"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7606b05842fea68ddcc89e8053b8860ebcb2a0ba8d6abfe3a148e5d5a8d3f0c1"
|
||||
dependencies = [
|
||||
"com_macros_support",
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "com_macros_support"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97e9a6d20f4ac8830e309a455d7e9416e65c6af5a97c88c55fbb4c2012e107da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-sha1"
|
||||
version = "0.2.0"
|
||||
@@ -270,10 +301,25 @@ checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.2.10",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
|
||||
|
||||
[[package]]
|
||||
name = "fs-tail"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73c8ee8694b2ad6d79aa976ad8572ca376c0450290041e6e3ae75147356b6ad2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent"
|
||||
version = "0.4.0"
|
||||
@@ -414,6 +460,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iovec"
|
||||
version = "0.1.4"
|
||||
@@ -441,7 +496,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebi"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"bitflags",
|
||||
@@ -456,6 +511,7 @@ dependencies = [
|
||||
"komorebi-core",
|
||||
"lazy_static",
|
||||
"nanoid",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -466,11 +522,12 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"uds_windows",
|
||||
"which",
|
||||
"winvd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
@@ -482,12 +539,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komorebic"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"bindings",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"dirs",
|
||||
"fs-tail",
|
||||
"komorebi-core",
|
||||
"paste",
|
||||
"powershell_script",
|
||||
@@ -514,6 +572,15 @@ version = "0.2.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
@@ -534,9 +601,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -691,9 +758,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.26.0"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386"
|
||||
checksum = "ee2766204889d09937d00bfbb7fec56bb2a199e2ade963cab19185d8a6104c7c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -716,12 +783,50 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"petgraph",
|
||||
"redox_syscall 0.2.10",
|
||||
"smallvec",
|
||||
"thread-id",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.7"
|
||||
@@ -884,6 +989,12 @@ dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
@@ -900,7 +1011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.2.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1044,9 +1155,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.74"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
|
||||
checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1096,6 +1207,17 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.1.57",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.3"
|
||||
@@ -1151,9 +1273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052"
|
||||
checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
@@ -1191,9 +1313,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.2.19"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48"
|
||||
checksum = "b9cbe87a2fa7e35900ce5de20220a582a9483a7063811defce79d7cbd59d4cfe"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"chrono",
|
||||
@@ -1349,6 +1471,17 @@ dependencies = [
|
||||
"windows_gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winvd"
|
||||
version = "0.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bab2d5c745381b9c72797230150ec62244e693064fa0d654b5c4e6c75132a56"
|
||||
dependencies = [
|
||||
"com",
|
||||
"crossbeam-channel",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ws2_32-sys"
|
||||
version = "0.2.1"
|
||||
|
||||
236
README.md
236
README.md
@@ -59,6 +59,8 @@ This means that:
|
||||
|
||||
## Getting Started
|
||||
|
||||
### GitHub Releases
|
||||
|
||||
Prebuilt binaries are available on the [releases page](https://github.com/LGUG2Z/komorebi/releases) in a `zip` archive.
|
||||
Once downloaded, you will need to move the `komorebi.exe` and `komorebic.exe` binaries to a directory in your `Path` (
|
||||
you can see these directories by running `$Env:Path.split(";")` at a PowerShell prompt).
|
||||
@@ -68,6 +70,8 @@ using [`setx`](https://docs.microsoft.com/en-us/windows-server/administration/wi
|
||||
Variables pop up in System Properties Advanced (which can be launched with `SystemPropertiesAdvanced.exe` at a
|
||||
PowerShell prompt), and then move the binaries to that directory.
|
||||
|
||||
### Scoop
|
||||
|
||||
If you use the [Scoop](https://scoop.sh/) command line installer, you can run the following commands to install the
|
||||
binaries from the latest GitHub Release:
|
||||
|
||||
@@ -79,6 +83,8 @@ scoop install komorebi
|
||||
If you install _komorebi_ using Scoop, the binaries will automatically be added to your `Path` and a command will be
|
||||
shown for you to run in order to get started using the sample configuration file.
|
||||
|
||||
### Building from Source
|
||||
|
||||
If you prefer to compile _komorebi_ from source, you will need
|
||||
a [working Rust development environment on Windows 10](https://rustup.rs/). The `x86_64-pc-windows-msvc` toolchain is
|
||||
required, so make sure you have also installed
|
||||
@@ -91,6 +97,8 @@ cargo install --path komorebi --locked
|
||||
cargo install --path komorebic --locked
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
Once you have either the prebuilt binaries in your `Path`, or have compiled the binaries from source (these will already
|
||||
be in your `Path` if you installed Rust with [rustup](https://rustup.rs), which you absolutely should), you can
|
||||
run `komorebic start` at a Powershell prompt, and you will see the following output:
|
||||
@@ -102,17 +110,45 @@ Start-Process komorebi -WindowStyle hidden
|
||||
This means that `komorebi` is now running in the background, tiling all your windows, and listening for commands sent to
|
||||
it by `komorebic`. You can similarly stop the process by running `komorebic stop`.
|
||||
|
||||
### Configuring
|
||||
|
||||
Once `komorebi` is running, you can execute the `komorebi.sample.ahk` script to set up the default keybindings via AHK
|
||||
(the file includes comments to help you start building your own configuration).
|
||||
|
||||
If you have AutoHotKey installed and a `komorebi.ahk` file in your home directory (run `$Env:UserProfile` at a
|
||||
PowerShell prompt to find your home directory), `komorebi` will automatically try to load it when starting.
|
||||
|
||||
There is also tentative support for loading a AutoHotKey v2, if the file is named `komorebi.ahk2` and
|
||||
There is also tentative support for loading a AutoHotKey v2 files, if the file is named `komorebi.ahk2` and
|
||||
the `AutoHotKey64.exe` executable for AutoHotKey v2 is in your `Path`. If both `komorebi.ahk` and `komorebi.ahk2` files
|
||||
exist in your home directory, only `komorebi.ahk` will be loaded. An example of an AutoHotKey v2 configuration file
|
||||
for _komorebi_ can be found [here](https://gist.github.com/crosstyan/dafacc0778dabf693ce9236c57b201cd).
|
||||
|
||||
### Common First-Time Tips
|
||||
|
||||
#### Floating Windows
|
||||
|
||||
Sometimes you will want a specific application to never be tiled, and instead float all the time. You add add rules to
|
||||
enforce this behaviour:
|
||||
|
||||
```powershell
|
||||
komorebic.exe float-rule title "Control Panel"
|
||||
# komorebic.exe float-rule exe [EXE NAME]
|
||||
# komorebic.exe float-rule class [CLASS NAME]
|
||||
```
|
||||
|
||||
#### Windows Not Getting Managed
|
||||
|
||||
In some rare cases, a window may not automatically be registered to be managed by `komorebi`. When this happens, you can
|
||||
manually add a rule to force `komorebi` to manage it:
|
||||
|
||||
```powershell
|
||||
komorebic.exe manage-rule exe TIM.exe
|
||||
# komorebic.exe manage-rule class [CLASS NAME]
|
||||
# komorebic.exe manage-rule title [TITLE]
|
||||
```
|
||||
|
||||
#### Tray Applications
|
||||
|
||||
If you are experiencing behaviour where
|
||||
[closing a window leaves a blank tile, but minimizing the same window does not](https://github.com/LGUG2Z/komorebi/issues/6)
|
||||
, you have probably enabled a 'close/minimize to tray' option for that application. You can tell _komorebi_ to handle
|
||||
@@ -120,10 +156,11 @@ this application appropriately by identifying it via the executable name or the
|
||||
|
||||
```powershell
|
||||
komorebic.exe identify-tray-application exe Discord.exe
|
||||
komorebic.exe identify-tray-application exe Telegram.exe
|
||||
# komorebic.exe identify-tray-application class [CLASS NAME]
|
||||
# komorebic.exe identify-tray-application title [TITLE]
|
||||
```
|
||||
|
||||
## Configuration
|
||||
## Configuration with `komorebic`
|
||||
|
||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||
personally use AutoHotKey to manage my window management shortcuts, and have provided a
|
||||
@@ -133,6 +170,52 @@ You can run `komorebic.exe` to get a full list of the commands that you can use
|
||||
keybindings with. You can run `komorebic.exe <COMMAND> --help` to get a full explanation of the arguments required for
|
||||
each command.
|
||||
|
||||
```
|
||||
start Start komorebi.exe as a background process
|
||||
stop Stop the komorebi.exe process and restore all hidden windows
|
||||
state Show a JSON representation of the current window manager state
|
||||
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
focus Change focus to the window in the specified direction
|
||||
move Move the focused window in the specified direction
|
||||
stack Stack the focused window in the specified direction
|
||||
resize Resize the focused window in the specified direction
|
||||
unstack Unstack the focused window
|
||||
cycle-stack Cycle the focused stack in the specified cycle direction
|
||||
move-to-monitor Move the focused window to the specified monitor
|
||||
move-to-workspace Move the focused window to the specified workspace
|
||||
focus-monitor Focus the specified monitor
|
||||
focus-workspace Focus the specified workspace on the focused monitor
|
||||
new-workspace Create and append a new workspace on the focused monitor
|
||||
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
|
||||
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
|
||||
ensure-workspaces Create at least this many workspaces for the specified monitor
|
||||
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-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
|
||||
toggle-tiling Toggle window tiling on the focused workspace
|
||||
toggle-float Toggle floating mode for the focused window
|
||||
toggle-monocle Toggle monocle mode for the focused container
|
||||
toggle-maximize Toggle native maximization for the focused window
|
||||
restore-windows Restore all hidden windows (debugging command)
|
||||
manage Force komorebi to manage the focused window
|
||||
unmanage Unmanage a window that was forcibly managed
|
||||
reload-configuration Reload ~/komorebi.ahk (if it exists)
|
||||
watch-configuration Toggle the automatic reloading of ~/komorebi.ahk (if it exists)
|
||||
float-rule Add a rule to always float the specified application
|
||||
manage-rule Add a rule to always manage the specified application
|
||||
workspace-rule Add a rule to associate an application with a workspace
|
||||
identify-tray-application Identify an application that closes to the system tray
|
||||
focus-follows-mouse Enable or disable focus follows mouse for the operating system
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Multi-monitor
|
||||
@@ -152,12 +235,13 @@ each command.
|
||||
- [x] BSP tree layout
|
||||
- [x] Flip BSP tree layout horizontally or vertically
|
||||
- [x] Equal-width, max-height column layout
|
||||
- [x] Floating rules based on exe name
|
||||
- [x] Floating rules based on window title
|
||||
- [x] Floating rules based on window class
|
||||
- [x] Identify 'close/minimize to tray' applications
|
||||
- [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 'close/minimize to tray' applications by exe name and class
|
||||
- [x] Toggle floating windows
|
||||
- [x] Toggle monocle window
|
||||
- [x] Toggle native maximization
|
||||
- [x] Toggle focus follows mouse
|
||||
- [x] Toggle automatic tiling
|
||||
- [x] Pause all window management
|
||||
@@ -184,7 +268,7 @@ If you use IntelliJ, you should enable the following settings to ensure that cod
|
||||
the IDE for completions and navigation:
|
||||
|
||||
- Set `Expand declarative macros`
|
||||
to `Use new engine` [here](jetbrains://idea/settings?name=Languages+%26+Frameworks--Rust)
|
||||
to `Use new engine` under "Settings > Langauges & Frameworks > Rust"
|
||||
- Enable the following experimental features:
|
||||
- `org.rust.cargo.evaluate.build.scripts`
|
||||
- `org.rust.macros.proc`
|
||||
@@ -200,134 +284,26 @@ ensures that all hidden windows are restored before termination.
|
||||
If however, you ever end up with windows that are hidden and cannot be restored, a list of window handles known
|
||||
to `komorebi` are stored and continuously updated in `~/komorebi.hwnd.json`.
|
||||
|
||||
### Restoring Windows
|
||||
|
||||
Running `komorebic restore-windows` will read the list of window handles and forcibly restore them, regardless of
|
||||
whether the main `komorebi` process is running.
|
||||
|
||||
### Panics and Deadlocks
|
||||
|
||||
If `komorebi` ever stops responding, it is most likely either due to either a panic or a deadlock. In the case of a
|
||||
panic, this will be reported in the log. In the case of a deadlock, there will not be any errors in the log, but the
|
||||
process and the log will appear frozen.
|
||||
|
||||
If you believe you have encountered a deadlock, you can compile `komorebi` with `--features deadlock_detection` and try
|
||||
reproducing the deadlock again. This will check for deadlocks every 5 seconds in the background, and if a deadlock is
|
||||
found, information about it will appear in the log which can be shared when opening an issu which can be shared when
|
||||
opening an issue.
|
||||
|
||||
## Window Manager State and Integrations
|
||||
|
||||
The current state of the window manager can be queried using the `komorebic state` command, which returns a JSON
|
||||
representation of the `WindowManager` struct.
|
||||
representation of the `State` struct, which includes the current state of `WindowManager`.
|
||||
|
||||
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).
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": {
|
||||
"elements": [
|
||||
{
|
||||
"id": 65537,
|
||||
"monitor_size": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"right": 3840,
|
||||
"bottom": 2160
|
||||
},
|
||||
"work_area_size": {
|
||||
"left": 0,
|
||||
"top": 40,
|
||||
"right": 3840,
|
||||
"bottom": 2120
|
||||
},
|
||||
"workspaces": {
|
||||
"elements": [
|
||||
{
|
||||
"name": "bsp",
|
||||
"containers": {
|
||||
"elements": [
|
||||
{
|
||||
"windows": {
|
||||
"elements": [
|
||||
{
|
||||
"hwnd": 2623596,
|
||||
"title": "komorebi – README.md",
|
||||
"exe": "idea64.exe",
|
||||
"class": "SunAwtFrame",
|
||||
"rect": {
|
||||
"left": 8,
|
||||
"top": 60,
|
||||
"right": 1914,
|
||||
"bottom": 2092
|
||||
}
|
||||
}
|
||||
],
|
||||
"focused": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"windows": {
|
||||
"elements": [
|
||||
{
|
||||
"hwnd": 198266,
|
||||
"title": "LGUG2Z/komorebi: A(nother) tiling window manager for Windows 10 based on binary space partitioning - Mozilla Firefox",
|
||||
"exe": "firefox.exe",
|
||||
"class": "MozillaWindowClass",
|
||||
"rect": {
|
||||
"left": 1918,
|
||||
"top": 60,
|
||||
"right": 1914,
|
||||
"bottom": 1042
|
||||
}
|
||||
}
|
||||
],
|
||||
"focused": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"windows": {
|
||||
"elements": [
|
||||
{
|
||||
"hwnd": 1247352,
|
||||
"title": "Windows PowerShell",
|
||||
"exe": "WindowsTerminal.exe",
|
||||
"class": "CASCADIA_HOSTING_WINDOW_CLASS",
|
||||
"rect": {
|
||||
"left": 1918,
|
||||
"top": 1110,
|
||||
"right": 959,
|
||||
"bottom": 1042
|
||||
}
|
||||
}
|
||||
],
|
||||
"focused": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"windows": {
|
||||
"elements": [
|
||||
{
|
||||
"hwnd": 395464,
|
||||
"title": "Signal",
|
||||
"exe": "Signal.exe",
|
||||
"class": "Chrome_WidgetWin_1",
|
||||
"rect": {
|
||||
"left": 2873,
|
||||
"top": 1110,
|
||||
"right": 959,
|
||||
"bottom": 1042
|
||||
}
|
||||
}
|
||||
],
|
||||
"focused": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"focused": 2
|
||||
},
|
||||
"monocle_container": null,
|
||||
"floating_windows": [],
|
||||
"layout": "BSP",
|
||||
"layout_flip": null,
|
||||
"workspace_padding": 10,
|
||||
"container_padding": 10
|
||||
}
|
||||
],
|
||||
"focused": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"focused": 0
|
||||
},
|
||||
"is_paused": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -12,7 +12,8 @@ pub enum CycleDirection {
|
||||
}
|
||||
|
||||
impl CycleDirection {
|
||||
pub fn next_idx(&self, idx: usize, len: usize) -> usize {
|
||||
#[must_use]
|
||||
pub const fn next_idx(&self, idx: usize, len: usize) -> usize {
|
||||
match self {
|
||||
CycleDirection::Previous => {
|
||||
if idx == 0 {
|
||||
|
||||
@@ -20,13 +20,15 @@ pub enum Layout {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum LayoutFlip {
|
||||
pub enum Flip {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
HorizontalAndVertical,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn resize(
|
||||
&self,
|
||||
unaltered: &Rect,
|
||||
@@ -35,10 +37,14 @@ impl Layout {
|
||||
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 = if let Some(step) = step { step } else { 50 };
|
||||
let resize_step = step.unwrap_or(50);
|
||||
|
||||
match edge {
|
||||
OperationDirection::Left => match sizing {
|
||||
@@ -117,19 +123,21 @@ impl Layout {
|
||||
},
|
||||
};
|
||||
|
||||
if !r.eq(&Rect::default()) {
|
||||
Option::from(r)
|
||||
} else {
|
||||
if r.eq(&Rect::default()) {
|
||||
None
|
||||
} else {
|
||||
Option::from(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[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<LayoutFlip>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
@@ -187,24 +195,6 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn next(&mut self) {
|
||||
match self {
|
||||
Layout::BSP => *self = Layout::Columns,
|
||||
Layout::Columns => *self = Layout::Rows,
|
||||
Layout::Rows => *self = Layout::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
match self {
|
||||
Layout::BSP => *self = Layout::Rows,
|
||||
Layout::Columns => *self = Layout::BSP,
|
||||
Layout::Rows => *self = Layout::Columns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
||||
let mut resize_adjustments = resize_dimensions.to_vec();
|
||||
|
||||
@@ -213,6 +203,7 @@ fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Optio
|
||||
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 {
|
||||
@@ -291,7 +282,7 @@ fn recursive_fibonacci(
|
||||
idx: usize,
|
||||
count: usize,
|
||||
area: &Rect,
|
||||
layout_flip: Option<LayoutFlip>,
|
||||
layout_flip: Option<Flip>,
|
||||
resize_adjustments: Vec<Option<Rect>>,
|
||||
) -> Vec<Rect> {
|
||||
let mut a = *area;
|
||||
@@ -313,37 +304,37 @@ fn recursive_fibonacci(
|
||||
|
||||
let (main_x, alt_x, alt_y, main_y);
|
||||
|
||||
match layout_flip {
|
||||
Some(flip) => match flip {
|
||||
LayoutFlip::Horizontal => {
|
||||
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;
|
||||
}
|
||||
LayoutFlip::Vertical => {
|
||||
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;
|
||||
}
|
||||
LayoutFlip::HorizontalAndVertical => {
|
||||
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;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
main_x = resized.left;
|
||||
alt_x = resized.left + half_resized_width;
|
||||
main_y = resized.top;
|
||||
alt_y = resized.top + half_resized_height;
|
||||
}
|
||||
} 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 {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ArgEnum;
|
||||
@@ -8,8 +11,8 @@ use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
pub use cycle_direction::CycleDirection;
|
||||
pub use layout::Flip;
|
||||
pub use layout::Layout;
|
||||
pub use layout::LayoutFlip;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use rect::Rect;
|
||||
|
||||
@@ -32,11 +35,14 @@ pub enum SocketMessage {
|
||||
Promote,
|
||||
ToggleFloat,
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
// Current Workspace Commands
|
||||
ManageFocusedWindow,
|
||||
UnmanageFocusedWindow,
|
||||
AdjustContainerPadding(Sizing, i32),
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(Layout),
|
||||
FlipLayout(LayoutFlip),
|
||||
FlipLayout(Flip),
|
||||
// Monitor and Workspace Commands
|
||||
EnsureWorkspaces(usize, usize),
|
||||
NewWorkspace,
|
||||
@@ -54,9 +60,9 @@ pub enum SocketMessage {
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
WatchConfiguration(bool),
|
||||
FloatClass(String),
|
||||
FloatExe(String),
|
||||
FloatTitle(String),
|
||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||
State,
|
||||
FocusFollowsMouse(bool),
|
||||
@@ -96,7 +102,8 @@ pub enum Sizing {
|
||||
}
|
||||
|
||||
impl Sizing {
|
||||
pub fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
||||
#[must_use]
|
||||
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
||||
match self {
|
||||
Sizing::Increase => value + adjustment,
|
||||
Sizing::Decrease => {
|
||||
|
||||
@@ -4,8 +4,8 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::Flip;
|
||||
use crate::Layout;
|
||||
use crate::LayoutFlip;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
@@ -17,59 +17,46 @@ pub enum OperationDirection {
|
||||
}
|
||||
|
||||
impl OperationDirection {
|
||||
pub fn opposite(self) -> Self {
|
||||
#[must_use]
|
||||
pub const fn opposite(self) -> Self {
|
||||
match self {
|
||||
OperationDirection::Left => OperationDirection::Right,
|
||||
OperationDirection::Right => OperationDirection::Left,
|
||||
OperationDirection::Up => OperationDirection::Down,
|
||||
OperationDirection::Down => OperationDirection::Up,
|
||||
Self::Left => Self::Right,
|
||||
Self::Right => Self::Left,
|
||||
Self::Up => Self::Down,
|
||||
Self::Down => Self::Up,
|
||||
}
|
||||
}
|
||||
|
||||
fn flip_direction(
|
||||
direction: &OperationDirection,
|
||||
layout_flip: Option<LayoutFlip>,
|
||||
) -> OperationDirection {
|
||||
if let Some(flip) = layout_flip {
|
||||
match direction {
|
||||
OperationDirection::Left => match flip {
|
||||
LayoutFlip::Horizontal | LayoutFlip::HorizontalAndVertical => {
|
||||
OperationDirection::Right
|
||||
}
|
||||
_ => *direction,
|
||||
},
|
||||
OperationDirection::Right => match flip {
|
||||
LayoutFlip::Horizontal | LayoutFlip::HorizontalAndVertical => {
|
||||
OperationDirection::Left
|
||||
}
|
||||
_ => *direction,
|
||||
},
|
||||
OperationDirection::Up => match flip {
|
||||
LayoutFlip::Vertical | LayoutFlip::HorizontalAndVertical => {
|
||||
OperationDirection::Down
|
||||
}
|
||||
_ => *direction,
|
||||
},
|
||||
OperationDirection::Down => match flip {
|
||||
LayoutFlip::Vertical | LayoutFlip::HorizontalAndVertical => {
|
||||
OperationDirection::Up
|
||||
}
|
||||
_ => *direction,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
*direction
|
||||
}
|
||||
fn flip_direction(direction: Self, layout_flip: Option<Flip>) -> Self {
|
||||
layout_flip.map_or(direction, |flip| match direction {
|
||||
Self::Left => match flip {
|
||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Right,
|
||||
Flip::Vertical => direction,
|
||||
},
|
||||
Self::Right => match flip {
|
||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Left,
|
||||
Flip::Vertical => direction,
|
||||
},
|
||||
Self::Up => match flip {
|
||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Down,
|
||||
Flip::Horizontal => direction,
|
||||
},
|
||||
Self::Down => match flip {
|
||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Up,
|
||||
Flip::Horizontal => direction,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid(
|
||||
&self,
|
||||
self,
|
||||
layout: Layout,
|
||||
layout_flip: Option<LayoutFlip>,
|
||||
layout_flip: Option<Flip>,
|
||||
idx: usize,
|
||||
len: usize,
|
||||
) -> bool {
|
||||
match OperationDirection::flip_direction(self, layout_flip) {
|
||||
match Self::flip_direction(self, layout_flip) {
|
||||
OperationDirection::Up => match layout {
|
||||
Layout::BSP => len > 2 && idx != 0 && idx != 1,
|
||||
Layout::Columns => false,
|
||||
@@ -93,9 +80,10 @@ impl OperationDirection {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_idx(&self, layout: Layout, layout_flip: Option<LayoutFlip>, idx: usize) -> usize {
|
||||
match OperationDirection::flip_direction(self, layout_flip) {
|
||||
OperationDirection::Up => match layout {
|
||||
#[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
|
||||
@@ -106,11 +94,11 @@ impl OperationDirection {
|
||||
Layout::Columns => unreachable!(),
|
||||
Layout::Rows => idx - 1,
|
||||
},
|
||||
OperationDirection::Down => match layout {
|
||||
Self::Down => match layout {
|
||||
Layout::BSP | Layout::Rows => idx + 1,
|
||||
Layout::Columns => unreachable!(),
|
||||
},
|
||||
OperationDirection::Left => match layout {
|
||||
Self::Left => match layout {
|
||||
Layout::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
idx - 2
|
||||
@@ -121,7 +109,7 @@ impl OperationDirection {
|
||||
Layout::Columns => idx - 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
},
|
||||
OperationDirection::Right => match layout {
|
||||
Self::Right => match layout {
|
||||
Layout::BSP | Layout::Columns => idx + 1,
|
||||
Layout::Rows => unreachable!(),
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct Rect {
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Self {
|
||||
Rect {
|
||||
Self {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
@@ -23,7 +23,7 @@ impl Default for Rect {
|
||||
|
||||
impl From<RECT> for Rect {
|
||||
fn from(rect: RECT) -> Self {
|
||||
Rect {
|
||||
Self {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right - rect.left,
|
||||
@@ -42,7 +42,8 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_point(&self, point: (i32, i32)) -> bool {
|
||||
#[must_use]
|
||||
pub const fn contains_point(&self, point: (i32, i32)) -> bool {
|
||||
point.0 >= self.left
|
||||
&& point.0 <= self.left + self.right
|
||||
&& point.1 >= self.top
|
||||
|
||||
@@ -29,6 +29,10 @@ Run, komorebic.exe workspace-layout 0 1 columns, , Hide
|
||||
; Set the floaty layout to not tile any windows
|
||||
Run, komorebic.exe workspace-tiling 0 4 disable, , Hide
|
||||
|
||||
; Always show chat apps on the second workspace
|
||||
Run, komorebic.exe workspace-rule exe slack.exe 0 1, , Hide
|
||||
Run, komorebic.exe workspace-rule exe Discord.exe 0 1, , Hide
|
||||
|
||||
; Always float IntelliJ popups, matching on class
|
||||
Run, komorebic.exe float-rule class SunAwtDialog, , Hide
|
||||
; Always float Control Panel, matching on title
|
||||
@@ -42,6 +46,9 @@ Run, komorebic.exe float-rule exe wincompose.exe, , Hide
|
||||
Run, komorebic.exe float-rule title Calculator, , Hide
|
||||
Run, komorebic.exe float-rule exe 1Password.exe, , Hide
|
||||
|
||||
; Always manage forcibly these applications that don't automatically get picked up by komorebi
|
||||
Run, komorebic.exe manage-rule exe TIM.exe, , Hide
|
||||
|
||||
; Identify applications that close to the tray
|
||||
Run, komorebic.exe identify-tray-application exe Discord.exe, , Hide
|
||||
|
||||
@@ -129,6 +136,11 @@ return
|
||||
Run, komorebic.exe toggle-monocle, , Hide
|
||||
return
|
||||
|
||||
; Toggle native maximize for the focused window, Alt + Shift + =
|
||||
!+=::
|
||||
Run, komorebic.exe toggle-maximize, , Hide
|
||||
return
|
||||
|
||||
; Flip horizontally, Alt + X
|
||||
!x::
|
||||
Run, komorebic.exe flip-layout horizontal, , Hide
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -20,6 +20,7 @@ getset = "0.1"
|
||||
hotwatch = "0.4"
|
||||
lazy_static = "1"
|
||||
nanoid = "0.4"
|
||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
||||
paste = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -29,4 +30,8 @@ tracing = "0.1"
|
||||
tracing-appender = "0.1"
|
||||
tracing-subscriber = "0.2"
|
||||
uds_windows = "1"
|
||||
which = "4"
|
||||
which = "4"
|
||||
winvd = "0.0.20"
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use std::thread;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use lazy_static::lazy_static;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use sysinfo::SystemExt;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
@@ -18,6 +25,7 @@ use which::which;
|
||||
|
||||
use crate::process_command::listen_for_commands;
|
||||
use crate::process_event::listen_for_events;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
|
||||
@@ -40,9 +48,6 @@ mod winevent_listener;
|
||||
mod workspace;
|
||||
|
||||
lazy_static! {
|
||||
static ref FLOAT_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_TITLES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_EXE_WHITELIST: Arc<Mutex<Vec<String>>> =
|
||||
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
|
||||
@@ -54,15 +59,19 @@ lazy_static! {
|
||||
"chrome.exe".to_string(),
|
||||
"idea64.exe".to_string(),
|
||||
"ApplicationFrameHost.exe".to_string(),
|
||||
"steam.exe".to_string()
|
||||
"steam.exe".to_string(),
|
||||
]));
|
||||
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"firefox.exe".to_string(),
|
||||
"idea64.exe".to_string(),
|
||||
]));
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, (usize, usize)>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
}
|
||||
|
||||
fn setup() -> Result<WorkerGuard> {
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
@@ -75,7 +84,9 @@ fn setup() -> Result<WorkerGuard> {
|
||||
|
||||
let home = dirs::home_dir().context("there is no home directory")?;
|
||||
let appender = tracing_appender::rolling::never(home, "komorebi.log");
|
||||
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
||||
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::fmt::Subscriber::builder()
|
||||
@@ -85,6 +96,11 @@ fn setup() -> Result<WorkerGuard> {
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false),
|
||||
)
|
||||
.with(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.with_writer(color_non_blocking)
|
||||
.with_ansi(true),
|
||||
),
|
||||
)?;
|
||||
|
||||
@@ -113,7 +129,7 @@ fn setup() -> Result<WorkerGuard> {
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(guard)
|
||||
Ok((guard, color_guard))
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
@@ -154,6 +170,29 @@ pub fn load_configuration() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
#[tracing::instrument]
|
||||
fn detect_deadlocks() {
|
||||
// Create a background thread which checks for deadlocks every 10s
|
||||
thread::spawn(move || loop {
|
||||
tracing::info!("running deadlock detector");
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
let deadlocks = deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::error!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
tracing::error!("deadlock #{}", i);
|
||||
for t in threads {
|
||||
tracing::error!("thread id: {:#?}", t.thread_id());
|
||||
tracing::error!("{:#?}", t.backtrace());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn main() -> Result<()> {
|
||||
match std::env::args().count() {
|
||||
@@ -167,7 +206,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let _guard = setup()?;
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
let process_id = WindowsApi::current_process_id();
|
||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||
@@ -178,11 +220,11 @@ fn main() -> Result<()> {
|
||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||
winevent_listener.start();
|
||||
|
||||
let wm = Arc::new(Mutex::new(window_manager::new(Arc::new(Mutex::new(
|
||||
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||
incoming,
|
||||
)))?));
|
||||
|
||||
wm.lock().unwrap().init()?;
|
||||
wm.lock().init()?;
|
||||
listen_for_commands(wm.clone());
|
||||
listen_for_events(wm.clone());
|
||||
|
||||
@@ -203,7 +245,7 @@ fn main() -> Result<()> {
|
||||
"received ctrl-c, restoring all hidden windows and terminating process"
|
||||
);
|
||||
|
||||
wm.lock().unwrap().restore_all_windows();
|
||||
wm.lock().restore_all_windows();
|
||||
std::process::exit(130);
|
||||
}
|
||||
_ => Ok(()),
|
||||
|
||||
@@ -70,14 +70,23 @@ impl Monitor {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn move_container_to_workspace(
|
||||
&mut self,
|
||||
target_workspace_idx: usize,
|
||||
follow: bool,
|
||||
) -> Result<()> {
|
||||
let container = self
|
||||
let workspace = self
|
||||
.focused_workspace_mut()
|
||||
.context("there is no workspace")?
|
||||
.context("there is no workspace")?;
|
||||
|
||||
if workspace.maximized_window().is_some() {
|
||||
return Err(eyre::anyhow!(
|
||||
"cannot move native maximized window to another monitor or workspace"
|
||||
));
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.context("there is no container")?;
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use parking_lot::Mutex;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
@@ -16,17 +16,16 @@ use komorebi_core::SocketMessage;
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::FLOAT_CLASSES;
|
||||
use crate::FLOAT_EXES;
|
||||
use crate::FLOAT_TITLES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
||||
let listener = wm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.command_listener
|
||||
.try_clone()
|
||||
.expect("could not clone unix listener");
|
||||
@@ -35,7 +34,7 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
||||
tracing::info!("listening");
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(stream) => match wm.lock().unwrap().read_commands(stream) {
|
||||
Ok(stream) => match wm.lock().read_commands(stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
},
|
||||
@@ -51,6 +50,8 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
||||
impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn process_command(&mut self, message: SocketMessage) -> Result<()> {
|
||||
self.validate_virtual_desktop_id();
|
||||
|
||||
match message {
|
||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||
SocketMessage::FocusWindow(direction) => {
|
||||
@@ -66,28 +67,31 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::ToggleFloat => self.toggle_float()?,
|
||||
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
|
||||
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
|
||||
SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => {
|
||||
self.set_container_padding(monitor_idx, workspace_idx, size)?;
|
||||
}
|
||||
SocketMessage::WorkspacePadding(monitor_idx, workspace_idx, size) => {
|
||||
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
|
||||
}
|
||||
SocketMessage::FloatClass(target) => {
|
||||
let mut float_classes = FLOAT_CLASSES.lock().unwrap();
|
||||
if !float_classes.contains(&target) {
|
||||
float_classes.push(target);
|
||||
SocketMessage::WorkspaceRule(_, id, monitor_idx, workspace_idx) => {
|
||||
{
|
||||
let mut workspace_rules = WORKSPACE_RULES.lock();
|
||||
workspace_rules.insert(id, (monitor_idx, workspace_idx));
|
||||
}
|
||||
|
||||
self.enforce_workspace_rules()?;
|
||||
}
|
||||
SocketMessage::ManageRule(_, id) => {
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
if !manage_identifiers.contains(&id) {
|
||||
manage_identifiers.push(id);
|
||||
}
|
||||
}
|
||||
SocketMessage::FloatExe(target) => {
|
||||
let mut float_exes = FLOAT_EXES.lock().unwrap();
|
||||
if !float_exes.contains(&target) {
|
||||
float_exes.push(target);
|
||||
}
|
||||
}
|
||||
SocketMessage::FloatTitle(target) => {
|
||||
let mut float_titles = FLOAT_TITLES.lock().unwrap();
|
||||
if !float_titles.contains(&target) {
|
||||
float_titles.push(target);
|
||||
SocketMessage::FloatRule(_, id) => {
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
if !float_identifiers.contains(&id) {
|
||||
float_identifiers.push(id);
|
||||
}
|
||||
}
|
||||
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
|
||||
@@ -182,19 +186,25 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::IdentifyTrayApplication(identifier, id) => match identifier {
|
||||
ApplicationIdentifier::Exe => {
|
||||
let mut exes = TRAY_AND_MULTI_WINDOW_EXES.lock().unwrap();
|
||||
let mut exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
|
||||
if !exes.contains(&id) {
|
||||
exes.push(id);
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
let mut classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock().unwrap();
|
||||
let mut classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
|
||||
if !classes.contains(&id) {
|
||||
classes.push(id);
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Title => {}
|
||||
},
|
||||
SocketMessage::ManageFocusedWindow => {
|
||||
self.manage_focused_window()?;
|
||||
}
|
||||
SocketMessage::UnmanageFocusedWindow => {
|
||||
self.unmanage_focused_window()?;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("processed");
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::select;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
@@ -20,7 +20,7 @@ use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
let receiver = wm.lock().unwrap().incoming_events.lock().unwrap().clone();
|
||||
let receiver = wm.lock().incoming_events.lock().clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
tracing::info!("listening");
|
||||
@@ -28,7 +28,7 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
select! {
|
||||
recv(receiver) -> mut maybe_event => {
|
||||
if let Ok(event) = maybe_event.as_mut() {
|
||||
match wm.lock().unwrap().process_event(event) {
|
||||
match wm.lock().process_event(event) {
|
||||
Ok(()) => {},
|
||||
Err(error) => tracing::error!("{}", error)
|
||||
}
|
||||
@@ -48,6 +48,8 @@ impl WindowManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.validate_virtual_desktop_id();
|
||||
|
||||
// Make sure we have the most recently focused monitor from any event
|
||||
match event {
|
||||
WindowManagerEvent::FocusChange(_, window)
|
||||
@@ -79,33 +81,46 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
self.enforce_workspace_rules()?;
|
||||
|
||||
if matches!(event, WindowManagerEvent::MouseCapture(..)) {
|
||||
tracing::trace!("only reaping orphans for mouse capture event");
|
||||
tracing::trace!(
|
||||
"only reaping orphans and enforcing workspace rules for mouse capture event"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match event {
|
||||
WindowManagerEvent::Minimize(_, window) | WindowManagerEvent::Destroy(_, window) => {
|
||||
WindowManagerEvent::Minimize(_, window)
|
||||
| WindowManagerEvent::Destroy(_, window)
|
||||
| WindowManagerEvent::Unmanage(window) => {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
|
||||
WindowManagerEvent::Hide(_, window) => {
|
||||
let mut hide = false;
|
||||
// Some major applications unfortunately send the HIDE signal when they are being
|
||||
// minimized or destroyed. Applications that close to the tray also do the same,
|
||||
// and will have is_window() return true, as the process is still running even if
|
||||
// the window is not visible.
|
||||
let tray_and_multi_window_exes = TRAY_AND_MULTI_WINDOW_EXES.lock().unwrap();
|
||||
let tray_and_multi_window_classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock().unwrap();
|
||||
|
||||
// We don't want to purge windows that have been deliberately hidden by us, eg. when
|
||||
// they are not on the top of a container stack.
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock().unwrap();
|
||||
|
||||
if (!window.is_window() || tray_and_multi_window_exes.contains(&window.exe()?))
|
||||
|| tray_and_multi_window_classes.contains(&window.class()?)
|
||||
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
{
|
||||
let tray_and_multi_window_exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
|
||||
let tray_and_multi_window_classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
|
||||
|
||||
// We don't want to purge windows that have been deliberately hidden by us, eg. when
|
||||
// they are not on the top of a container stack.
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
|
||||
if (!window.is_window() || tray_and_multi_window_exes.contains(&window.exe()?))
|
||||
|| tray_and_multi_window_classes.contains(&window.class()?)
|
||||
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
{
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
if hide {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
@@ -120,10 +135,16 @@ impl WindowManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(w) = workspace.maximized_window() {
|
||||
if w.hwnd == window.hwnd {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
self.focused_workspace_mut()?
|
||||
.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
WindowManagerEvent::Show(_, window) => {
|
||||
WindowManagerEvent::Show(_, window) | WindowManagerEvent::Manage(window) => {
|
||||
let mut switch_to = None;
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitors.workspaces().iter().enumerate() {
|
||||
@@ -133,16 +154,16 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(indices) = switch_to {
|
||||
if self.focused_monitor_idx() != indices.0
|
||||
&& self
|
||||
if let Some((known_monitor_idx, known_workspace_idx)) = switch_to {
|
||||
if self.focused_monitor_idx() != known_monitor_idx
|
||||
|| self
|
||||
.focused_monitor()
|
||||
.context("there is no monitor")?
|
||||
.focused_workspace_idx()
|
||||
!= indices.1
|
||||
!= known_workspace_idx
|
||||
{
|
||||
self.focus_monitor(indices.0)?;
|
||||
self.focus_workspace(indices.1)?;
|
||||
self.focus_monitor(known_monitor_idx)?;
|
||||
self.focus_workspace(known_workspace_idx)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -170,7 +191,7 @@ impl WindowManager {
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
if workspace.containers().is_empty() || !workspace.contains_window(window.hwnd) {
|
||||
if !workspace.contains_window(window.hwnd) {
|
||||
workspace.new_container_for_window(*window);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
@@ -267,6 +288,11 @@ impl WindowManager {
|
||||
WindowManagerEvent::MouseCapture(..) => {}
|
||||
};
|
||||
|
||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||
if let WindowManagerEvent::Unmanage(window) = event {
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
}
|
||||
|
||||
tracing::trace!("updating list of known hwnds");
|
||||
let mut known_hwnds = vec![];
|
||||
for monitor in self.monitors() {
|
||||
|
||||
@@ -15,11 +15,10 @@ use crate::styles::GwlExStyle;
|
||||
use crate::styles::GwlStyle;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::FLOAT_CLASSES;
|
||||
use crate::FLOAT_EXES;
|
||||
use crate::FLOAT_TITLES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Window {
|
||||
@@ -71,6 +70,21 @@ impl Window {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
|
||||
self.set_position(
|
||||
&Rect {
|
||||
left: work_area.left + ((work_area.right - half_width) / 2),
|
||||
top: work_area.top + ((work_area.bottom - half_weight) / 2),
|
||||
right: half_width,
|
||||
bottom: half_weight,
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
|
||||
// NOTE: This is how the border variable below was calculated; every time this code was
|
||||
// run on any window in any position, the generated border was always the same, so I am
|
||||
@@ -110,7 +124,7 @@ impl Window {
|
||||
}
|
||||
|
||||
pub fn hide(self) {
|
||||
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock().unwrap();
|
||||
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
|
||||
programmatically_hidden_hwnds.push(self.hwnd);
|
||||
}
|
||||
@@ -119,7 +133,7 @@ impl Window {
|
||||
}
|
||||
|
||||
pub fn restore(self) {
|
||||
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock().unwrap();
|
||||
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
if let Some(idx) = programmatically_hidden_hwnds
|
||||
.iter()
|
||||
.position(|&hwnd| hwnd == self.hwnd)
|
||||
@@ -130,6 +144,18 @@ impl Window {
|
||||
WindowsApi::restore_window(self.hwnd());
|
||||
}
|
||||
|
||||
pub fn maximize(self) {
|
||||
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
if let Some(idx) = programmatically_hidden_hwnds
|
||||
.iter()
|
||||
.position(|&hwnd| hwnd == self.hwnd)
|
||||
{
|
||||
programmatically_hidden_hwnds.remove(idx);
|
||||
}
|
||||
|
||||
WindowsApi::maximize_window(self.hwnd());
|
||||
}
|
||||
|
||||
pub fn focus(self) -> Result<()> {
|
||||
// Attach komorebi thread to Window thread
|
||||
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
@@ -192,10 +218,6 @@ impl Window {
|
||||
|
||||
#[tracing::instrument(fields(exe, title))]
|
||||
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
|
||||
let classes = FLOAT_CLASSES.lock().unwrap();
|
||||
let exes = FLOAT_EXES.lock().unwrap();
|
||||
let titles = FLOAT_TITLES.lock().unwrap();
|
||||
|
||||
if self.title().is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -212,22 +234,25 @@ impl Window {
|
||||
(true, _) |
|
||||
// If not allowing cloaked windows, we need to ensure the window is not cloaked
|
||||
(false, false) => {
|
||||
if let (Ok(title), Ok(exe_name)) = (self.title(), self.exe()) {
|
||||
if titles.contains(&title) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if exes.contains(&exe_name) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Ok(class) = self.class() {
|
||||
if classes.contains(&class) {
|
||||
if let (Ok(title), Ok(exe_name), Ok(class)) = (self.title(), self.exe(), self.class()) {
|
||||
{
|
||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
if float_identifiers.contains(&title)
|
||||
|| float_identifiers.contains(&exe_name)
|
||||
|| float_identifiers.contains(&class) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let allow_layered = LAYERED_EXE_WHITELIST.lock().unwrap().contains(&exe_name);
|
||||
let managed_override = {
|
||||
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
manage_identifiers.contains(&exe_name) || manage_identifiers.contains(&class)
|
||||
};
|
||||
|
||||
let allow_layered = {
|
||||
let layered_exe_whitelist = LAYERED_EXE_WHITELIST.lock();
|
||||
layered_exe_whitelist.contains(&exe_name)
|
||||
};
|
||||
|
||||
let style = self.style()?;
|
||||
let ex_style = self.ex_style()?;
|
||||
@@ -240,20 +265,17 @@ impl Window {
|
||||
// allowing a specific layered window on the whitelist (like Steam), it should
|
||||
// pass this check
|
||||
&& (allow_layered || !ex_style.contains(GwlExStyle::LAYERED))
|
||||
|| managed_override
|
||||
{
|
||||
Ok(true)
|
||||
} else {
|
||||
if event.is_some() {
|
||||
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
return Ok(true);
|
||||
} else if event.is_some() {
|
||||
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
|
||||
}
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
_ => Ok(false),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::io::ErrorKind;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
@@ -11,12 +10,13 @@ use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
use hotwatch::Hotwatch;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::LayoutFlip;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
@@ -28,13 +28,14 @@ use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::FLOAT_CLASSES;
|
||||
use crate::FLOAT_EXES;
|
||||
use crate::FLOAT_TITLES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::LAYERED_EXE_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowManager {
|
||||
@@ -43,15 +44,15 @@ pub struct WindowManager {
|
||||
pub command_listener: UnixListener,
|
||||
pub is_paused: bool,
|
||||
pub hotwatch: Hotwatch,
|
||||
pub virtual_desktop_id: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub is_paused: bool,
|
||||
pub float_classes: Vec<String>,
|
||||
pub float_exes: Vec<String>,
|
||||
pub float_titles: Vec<String>,
|
||||
pub float_identifiers: Vec<String>,
|
||||
pub manage_identifiers: Vec<String>,
|
||||
pub layered_exe_whitelist: Vec<String>,
|
||||
pub tray_and_multi_window_exes: Vec<String>,
|
||||
pub tray_and_multi_window_classes: Vec<String>,
|
||||
@@ -63,48 +64,74 @@ impl From<&mut WindowManager> for State {
|
||||
Self {
|
||||
monitors: wm.monitors.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
float_classes: FLOAT_CLASSES.lock().unwrap().clone(),
|
||||
float_exes: FLOAT_EXES.lock().unwrap().clone(),
|
||||
float_titles: FLOAT_TITLES.lock().unwrap().clone(),
|
||||
layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().unwrap().clone(),
|
||||
tray_and_multi_window_exes: TRAY_AND_MULTI_WINDOW_EXES.lock().unwrap().clone(),
|
||||
tray_and_multi_window_classes: TRAY_AND_MULTI_WINDOW_CLASSES.lock().unwrap().clone(),
|
||||
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().clone(),
|
||||
tray_and_multi_window_exes: TRAY_AND_MULTI_WINDOW_EXES.lock().clone(),
|
||||
tray_and_multi_window_classes: TRAY_AND_MULTI_WINDOW_CLASSES.lock().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_ring_elements!(WindowManager, Monitor);
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn new(incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>) -> Result<WindowManager> {
|
||||
let home = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut socket = home;
|
||||
socket.push("komorebi.sock");
|
||||
let socket = socket.as_path();
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct EnforceWorkspaceRuleOp {
|
||||
hwnd: isize,
|
||||
origin_monitor_idx: usize,
|
||||
origin_workspace_idx: usize,
|
||||
target_monitor_idx: usize,
|
||||
target_workspace_idx: usize,
|
||||
}
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
impl EnforceWorkspaceRuleOp {
|
||||
const fn is_origin(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
|
||||
self.origin_monitor_idx == monitor_idx && self.origin_workspace_idx == workspace_idx
|
||||
}
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
const fn is_target(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
|
||||
self.target_monitor_idx == monitor_idx && self.target_workspace_idx == workspace_idx
|
||||
}
|
||||
|
||||
Ok(WindowManager {
|
||||
monitors: Ring::default(),
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
hotwatch: Hotwatch::new()?,
|
||||
})
|
||||
const fn is_enforced(&self) -> bool {
|
||||
(self.origin_monitor_idx == self.target_monitor_idx)
|
||||
&& (self.origin_workspace_idx == self.target_workspace_idx)
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowManager {
|
||||
#[tracing::instrument]
|
||||
pub fn new(incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>) -> Result<Self> {
|
||||
let home = dirs::home_dir().context("there is no home directory")?;
|
||||
let mut socket = home;
|
||||
socket.push("komorebi.sock");
|
||||
let socket = socket.as_path();
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(_) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
let virtual_desktop_id = winvd::helpers::get_current_desktop_number().ok();
|
||||
|
||||
Ok(Self {
|
||||
monitors: Ring::default(),
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
hotwatch: Hotwatch::new()?,
|
||||
virtual_desktop_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn init(&mut self) -> Result<()> {
|
||||
tracing::info!("initialising");
|
||||
@@ -187,6 +214,151 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn enforce_workspace_rules(&mut self) -> Result<()> {
|
||||
let mut to_move = vec![];
|
||||
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self
|
||||
.monitors()
|
||||
.get(focused_monitor_idx)
|
||||
.context("there is no monitor with that index")?
|
||||
.focused_workspace_idx();
|
||||
|
||||
let workspace_rules = WORKSPACE_RULES.lock();
|
||||
// Go through all the monitors and workspaces
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
// And all the visible windows (at the top of a container)
|
||||
for window in workspace.visible_windows().into_iter().flatten() {
|
||||
// If the executable names or titles of any of those windows are in our rules map
|
||||
if let Some((monitor_idx, workspace_idx)) = workspace_rules.get(&window.exe()?)
|
||||
{
|
||||
tracing::info!(
|
||||
"{} should be on monitor {}, workspace {}",
|
||||
window.title()?,
|
||||
*monitor_idx,
|
||||
*workspace_idx
|
||||
);
|
||||
|
||||
// Create an operation outline and save it for later in the fn
|
||||
to_move.push(EnforceWorkspaceRuleOp {
|
||||
hwnd: window.hwnd,
|
||||
origin_monitor_idx: i,
|
||||
origin_workspace_idx: j,
|
||||
target_monitor_idx: *monitor_idx,
|
||||
target_workspace_idx: *workspace_idx,
|
||||
});
|
||||
} else if let Some((monitor_idx, workspace_idx)) =
|
||||
workspace_rules.get(&window.title()?)
|
||||
{
|
||||
tracing::info!(
|
||||
"{} should be on monitor {}, workspace {}",
|
||||
window.title()?,
|
||||
*monitor_idx,
|
||||
*workspace_idx
|
||||
);
|
||||
|
||||
to_move.push(EnforceWorkspaceRuleOp {
|
||||
hwnd: window.hwnd,
|
||||
origin_monitor_idx: i,
|
||||
origin_workspace_idx: j,
|
||||
target_monitor_idx: *monitor_idx,
|
||||
target_workspace_idx: *workspace_idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only retain operations where the target is not the current workspace
|
||||
to_move.retain(|op| !op.is_target(focused_monitor_idx, focused_workspace_idx));
|
||||
// Only retain operations where the rule has not already been enforced
|
||||
to_move.retain(|op| !op.is_enforced());
|
||||
|
||||
let mut should_update_focused_workspace = false;
|
||||
|
||||
// Parse the operation and remove any windows that are not placed according to their rules
|
||||
for op in &to_move {
|
||||
let origin_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(op.origin_monitor_idx)
|
||||
.context("there is no monitor with that index")?
|
||||
.workspaces_mut()
|
||||
.get_mut(op.origin_workspace_idx)
|
||||
.context("there is no workspace with that index")?;
|
||||
|
||||
// Hide the window we are about to remove if it is on the currently focused workspace
|
||||
if op.is_origin(focused_monitor_idx, focused_workspace_idx) {
|
||||
Window { hwnd: op.hwnd }.hide();
|
||||
should_update_focused_workspace = true;
|
||||
}
|
||||
|
||||
origin_workspace.remove_window(op.hwnd)?;
|
||||
}
|
||||
|
||||
// Parse the operation again and associate those removed windows with the workspace that
|
||||
// their rules have defined for them
|
||||
for op in &to_move {
|
||||
let target_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(op.target_monitor_idx)
|
||||
.context("there is no monitor with that index")?;
|
||||
|
||||
// The very first time this fn is called, the workspace might not even exist yet
|
||||
if target_monitor
|
||||
.workspaces()
|
||||
.get(op.target_workspace_idx)
|
||||
.is_none()
|
||||
{
|
||||
// If it doesn't, let's make sure it does for the next step
|
||||
target_monitor.ensure_workspace_count(op.target_workspace_idx + 1);
|
||||
}
|
||||
|
||||
let target_workspace = target_monitor
|
||||
.workspaces_mut()
|
||||
.get_mut(op.target_workspace_idx)
|
||||
.context("there is no workspace with that index")?;
|
||||
|
||||
target_workspace.new_container_for_window(Window { hwnd: op.hwnd });
|
||||
}
|
||||
|
||||
// Only re-tile the focused workspace if we need to
|
||||
if should_update_focused_workspace {
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn validate_virtual_desktop_id(&self) {
|
||||
let virtual_desktop_id = winvd::helpers::get_current_desktop_number().ok();
|
||||
if let (Some(id), Some(virtual_desktop_id)) = (virtual_desktop_id, self.virtual_desktop_id)
|
||||
{
|
||||
if id != virtual_desktop_id {
|
||||
tracing::warn!(
|
||||
"ignoring events while not on virtual desktop {}",
|
||||
virtual_desktop_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn manage_focused_window(&mut self) -> Result<()> {
|
||||
let hwnd = WindowsApi::foreground_window()?;
|
||||
let event = WindowManagerEvent::Manage(Window { hwnd });
|
||||
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn unmanage_focused_window(&mut self) -> Result<()> {
|
||||
let hwnd = WindowsApi::foreground_window()?;
|
||||
let event = WindowManagerEvent::Unmanage(Window { hwnd });
|
||||
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn update_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
tracing::info!("updating");
|
||||
@@ -196,7 +368,13 @@ impl WindowManager {
|
||||
.update_focused_workspace()?;
|
||||
|
||||
if mouse_follows_focus {
|
||||
if let Ok(window) = self.focused_window_mut() {
|
||||
if let Some(window) = self.focused_workspace()?.maximized_window() {
|
||||
window.focus()?;
|
||||
} else if let Some(container) = self.focused_workspace()?.monocle_container() {
|
||||
if let Some(window) = container.focused_window() {
|
||||
window.focus()?;
|
||||
}
|
||||
} else if let Ok(window) = self.focused_window_mut() {
|
||||
window.focus()?;
|
||||
} else {
|
||||
let desktop_window = Window {
|
||||
@@ -254,21 +432,21 @@ impl WindowManager {
|
||||
// can flip them however they need to be flipped once the resizing has been done
|
||||
if let Some(flip) = workspace.layout_flip() {
|
||||
match flip {
|
||||
LayoutFlip::Horizontal => {
|
||||
Flip::Horizontal => {
|
||||
if matches!(direction, OperationDirection::Left)
|
||||
|| matches!(direction, OperationDirection::Right)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
LayoutFlip::Vertical => {
|
||||
Flip::Vertical => {
|
||||
if matches!(direction, OperationDirection::Up)
|
||||
|| matches!(direction, OperationDirection::Down)
|
||||
{
|
||||
direction = direction.opposite();
|
||||
}
|
||||
}
|
||||
LayoutFlip::HorizontalAndVertical => direction = direction.opposite(),
|
||||
Flip::HorizontalAndVertical => direction = direction.opposite(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,9 +488,17 @@ impl WindowManager {
|
||||
tracing::info!("moving container");
|
||||
|
||||
let monitor = self.focused_monitor_mut().context("there is no monitor")?;
|
||||
let container = monitor
|
||||
let workspace = monitor
|
||||
.focused_workspace_mut()
|
||||
.context("there is no workspace")?
|
||||
.context("there is no workspace")?;
|
||||
|
||||
if workspace.maximized_window().is_some() {
|
||||
return Err(eyre::anyhow!(
|
||||
"cannot move native maximized window to another monitor or workspace"
|
||||
));
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.context("there is no container")?;
|
||||
|
||||
@@ -455,7 +641,7 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn toggle_float(&mut self) -> Result<()> {
|
||||
let hwnd = WindowsApi::top_visible_window()?;
|
||||
let hwnd = WindowsApi::foreground_window()?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let mut is_floating_window = false;
|
||||
@@ -468,11 +654,11 @@ impl WindowManager {
|
||||
|
||||
if is_floating_window {
|
||||
self.unfloat_window()?;
|
||||
self.update_focused_workspace(true)
|
||||
} else {
|
||||
self.float_window()?;
|
||||
self.update_focused_workspace(false)
|
||||
}
|
||||
|
||||
self.update_focused_workspace(is_floating_window)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -489,17 +675,7 @@ impl WindowManager {
|
||||
.last_mut()
|
||||
.context("there is no floating window")?;
|
||||
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
|
||||
let center = Rect {
|
||||
left: work_area.left + ((work_area.right - half_width) / 2),
|
||||
top: work_area.top + ((work_area.bottom - half_weight) / 2),
|
||||
right: half_width,
|
||||
bottom: half_weight,
|
||||
};
|
||||
|
||||
window.set_position(¢er, true)?;
|
||||
window.center(&work_area)?;
|
||||
window.focus()?;
|
||||
|
||||
Ok(())
|
||||
@@ -542,7 +718,35 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn flip_layout(&mut self, layout_flip: LayoutFlip) -> Result<()> {
|
||||
pub fn toggle_maximize(&mut self) -> Result<()> {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
match workspace.maximized_window() {
|
||||
None => self.maximize_window()?,
|
||||
Some(_) => self.unmaximize_window()?,
|
||||
}
|
||||
|
||||
self.update_focused_workspace(false)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn maximize_window(&mut self) -> Result<()> {
|
||||
tracing::info!("maximizing windowj");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.new_maximized_window()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn unmaximize_window(&mut self) -> Result<()> {
|
||||
tracing::info!("unmaximizing window");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.reintegrate_maximized_window()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn flip_layout(&mut self, layout_flip: Flip) -> Result<()> {
|
||||
tracing::info!("flipping layout");
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
@@ -554,28 +758,28 @@ impl WindowManager {
|
||||
}
|
||||
Some(current_layout_flip) => {
|
||||
match current_layout_flip {
|
||||
LayoutFlip::Horizontal => match layout_flip {
|
||||
LayoutFlip::Horizontal => workspace.set_layout_flip(None),
|
||||
LayoutFlip::Vertical => workspace
|
||||
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
|
||||
LayoutFlip::HorizontalAndVertical => workspace
|
||||
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
|
||||
},
|
||||
LayoutFlip::Vertical => match layout_flip {
|
||||
LayoutFlip::Horizontal => workspace
|
||||
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
|
||||
LayoutFlip::Vertical => workspace.set_layout_flip(None),
|
||||
LayoutFlip::HorizontalAndVertical => workspace
|
||||
.set_layout_flip(Option::from(LayoutFlip::HorizontalAndVertical)),
|
||||
},
|
||||
LayoutFlip::HorizontalAndVertical => match layout_flip {
|
||||
LayoutFlip::Horizontal => {
|
||||
workspace.set_layout_flip(Option::from(LayoutFlip::Vertical))
|
||||
Flip::Horizontal => match layout_flip {
|
||||
Flip::Horizontal => workspace.set_layout_flip(None),
|
||||
Flip::Vertical => {
|
||||
workspace.set_layout_flip(Option::from(Flip::HorizontalAndVertical))
|
||||
}
|
||||
LayoutFlip::Vertical => {
|
||||
workspace.set_layout_flip(Option::from(LayoutFlip::Horizontal))
|
||||
Flip::HorizontalAndVertical => {
|
||||
workspace.set_layout_flip(Option::from(Flip::HorizontalAndVertical))
|
||||
}
|
||||
LayoutFlip::HorizontalAndVertical => workspace.set_layout_flip(None),
|
||||
},
|
||||
Flip::Vertical => match layout_flip {
|
||||
Flip::Horizontal => {
|
||||
workspace.set_layout_flip(Option::from(Flip::HorizontalAndVertical))
|
||||
}
|
||||
Flip::Vertical => workspace.set_layout_flip(None),
|
||||
Flip::HorizontalAndVertical => {
|
||||
workspace.set_layout_flip(Option::from(Flip::HorizontalAndVertical))
|
||||
}
|
||||
},
|
||||
Flip::HorizontalAndVertical => match layout_flip {
|
||||
Flip::Horizontal => workspace.set_layout_flip(Option::from(Flip::Vertical)),
|
||||
Flip::Vertical => workspace.set_layout_flip(Option::from(Flip::Horizontal)),
|
||||
Flip::HorizontalAndVertical => workspace.set_layout_flip(None),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::fmt::Formatter;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum WindowManagerEvent {
|
||||
@@ -13,11 +14,19 @@ pub enum WindowManagerEvent {
|
||||
Show(WinEvent, Window),
|
||||
MoveResizeEnd(WinEvent, Window),
|
||||
MouseCapture(WinEvent, Window),
|
||||
Manage(Window),
|
||||
Unmanage(Window),
|
||||
}
|
||||
|
||||
impl Display for WindowManagerEvent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
WindowManagerEvent::Manage(window) => {
|
||||
write!(f, "Manage (Window: {})", window)
|
||||
}
|
||||
WindowManagerEvent::Unmanage(window) => {
|
||||
write!(f, "Unmanage (Window: {})", window)
|
||||
}
|
||||
WindowManagerEvent::Destroy(winevent, window) => {
|
||||
write!(f, "Destroy (WinEvent: {}, Window: {})", winevent, window)
|
||||
}
|
||||
@@ -64,28 +73,48 @@ impl WindowManagerEvent {
|
||||
| WindowManagerEvent::Minimize(_, window)
|
||||
| WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::MoveResizeEnd(_, window)
|
||||
| WindowManagerEvent::MouseCapture(_, window) => window,
|
||||
| WindowManagerEvent::MouseCapture(_, window)
|
||||
| WindowManagerEvent::Manage(window)
|
||||
| WindowManagerEvent::Unmanage(window) => window,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_win_event(winevent: WinEvent, window: Window) -> Option<Self> {
|
||||
pub fn from_win_event(winevent: WinEvent, window: Window) -> Option<Self> {
|
||||
match winevent {
|
||||
WinEvent::ObjectDestroy => Some(Self::Destroy(winevent, window)),
|
||||
WinEvent::ObjectDestroy => Option::from(Self::Destroy(winevent, window)),
|
||||
|
||||
WinEvent::ObjectCloaked | WinEvent::ObjectHide => Some(Self::Hide(winevent, window)),
|
||||
WinEvent::ObjectCloaked | WinEvent::ObjectHide => {
|
||||
Option::from(Self::Hide(winevent, window))
|
||||
}
|
||||
|
||||
WinEvent::SystemMinimizeStart => Some(Self::Minimize(winevent, window)),
|
||||
WinEvent::SystemMinimizeStart => Option::from(Self::Minimize(winevent, window)),
|
||||
|
||||
WinEvent::ObjectShow | WinEvent::ObjectUncloaked | WinEvent::SystemMinimizeEnd => {
|
||||
Some(Self::Show(winevent, window))
|
||||
Option::from(Self::Show(winevent, window))
|
||||
}
|
||||
|
||||
WinEvent::ObjectFocus | WinEvent::SystemForeground => {
|
||||
Some(Self::FocusChange(winevent, window))
|
||||
Option::from(Self::FocusChange(winevent, window))
|
||||
}
|
||||
WinEvent::SystemMoveSizeEnd => Some(Self::MoveResizeEnd(winevent, window)),
|
||||
WinEvent::SystemMoveSizeEnd => Option::from(Self::MoveResizeEnd(winevent, window)),
|
||||
WinEvent::SystemCaptureStart | WinEvent::SystemCaptureEnd => {
|
||||
Some(Self::MouseCapture(winevent, window))
|
||||
Option::from(Self::MouseCapture(winevent, window))
|
||||
}
|
||||
WinEvent::ObjectNameChange => {
|
||||
// Some apps like Firefox don't send ObjectCreate or ObjectShow on launch
|
||||
// This spams the message queue, but I don't know what else to do. On launch
|
||||
// it only sends the following WinEvents :/
|
||||
//
|
||||
// [yatta\src\windows_event.rs:110] event = 32780 ObjectNameChange
|
||||
// [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange
|
||||
|
||||
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
|
||||
if object_name_change_on_launch.contains(&window.exe().ok()?) {
|
||||
Option::from(Self::Show(winevent, window))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EnumWindows;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindow;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
@@ -68,6 +69,7 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS;
|
||||
@@ -259,6 +261,14 @@ impl WindowsApi {
|
||||
Self::show_window(hwnd, SW_RESTORE);
|
||||
}
|
||||
|
||||
pub fn maximize_window(hwnd: HWND) {
|
||||
Self::show_window(hwnd, SW_MAXIMIZE);
|
||||
}
|
||||
|
||||
pub fn foreground_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetForegroundWindow() }))
|
||||
}
|
||||
|
||||
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
|
||||
match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) {
|
||||
WindowsResult::Ok(_) => Ok(()),
|
||||
@@ -275,6 +285,7 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn top_window() -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe { GetTopWindow(HWND::NULL).0 }))
|
||||
}
|
||||
@@ -283,12 +294,14 @@ impl WindowsApi {
|
||||
Result::from(WindowsResult::from(unsafe { GetDesktopWindow() }))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn next_window(hwnd: HWND) -> Result<isize> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindow(hwnd, GW_HWNDNEXT).0
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn top_visible_window() -> Result<isize> {
|
||||
let hwnd = Self::top_window()?;
|
||||
let mut next_hwnd = hwnd;
|
||||
|
||||
@@ -14,9 +14,7 @@ use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
|
||||
pub extern "system" fn enum_display_monitor(
|
||||
hmonitor: HMONITOR,
|
||||
@@ -71,37 +69,15 @@ pub extern "system" fn win_event_hook(
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
|
||||
let winevent = unsafe { ::std::mem::transmute(event) };
|
||||
let event_type = if let Some(event) = WindowManagerEvent::from_win_event(winevent, window) {
|
||||
event
|
||||
} else {
|
||||
// Some apps like Firefox don't send ObjectCreate or ObjectShow on launch
|
||||
// This spams the message queue, but I don't know what else to do. On launch
|
||||
// it only sends the following WinEvents :/
|
||||
//
|
||||
// [yatta\src\windows_event.rs:110] event = 32780 ObjectNameChange
|
||||
// [yatta\src\windows_event.rs:110] event = 32779 ObjectLocationChange
|
||||
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock().unwrap();
|
||||
|
||||
if let Ok(exe) = window.exe() {
|
||||
if winevent == WinEvent::ObjectNameChange {
|
||||
if object_name_change_on_launch.contains(&exe) {
|
||||
WindowManagerEvent::Show(winevent, window)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
|
||||
None => return,
|
||||
Some(event) => event,
|
||||
};
|
||||
|
||||
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
|
||||
if should_manage {
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.unwrap()
|
||||
.0
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::sync::atomic::AtomicIsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use bindings::Windows::Win32::Foundation::HWND;
|
||||
use bindings::Windows::Win32::UI::Accessibility::SetWinEventHook;
|
||||
@@ -43,7 +43,7 @@ pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> WinEventListener
|
||||
impl WinEventListener {
|
||||
pub fn start(self) {
|
||||
let hook = self.hook.clone();
|
||||
let outgoing = self.outgoing_events.lock().unwrap().clone();
|
||||
let outgoing = self.outgoing_events.lock().clone();
|
||||
|
||||
thread::spawn(move || unsafe {
|
||||
let hook_ref = SetWinEventHook(
|
||||
@@ -61,7 +61,7 @@ impl WinEventListener {
|
||||
// The code in the callback doesn't work in its own loop, needs to be within
|
||||
// the MessageLoop callback for the winevent callback to even fire
|
||||
MessageLoop::start(10, |_msg| {
|
||||
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().unwrap().1.try_recv() {
|
||||
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().1.try_recv() {
|
||||
match outgoing.send(event) {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
|
||||
@@ -9,8 +9,8 @@ use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::LayoutFlip;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
@@ -28,13 +28,18 @@ pub struct Workspace {
|
||||
monocle_container: Option<Container>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
monocle_restore_idx: Option<usize>,
|
||||
monocle_container_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
maximized_window: Option<Window>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
maximized_window_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
floating_windows: Vec<Window>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
layout: Layout,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
layout_flip: Option<LayoutFlip>,
|
||||
layout_flip: Option<Flip>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
workspace_padding: Option<i32>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
@@ -57,7 +62,9 @@ impl Default for Workspace {
|
||||
name: None,
|
||||
containers: Ring::default(),
|
||||
monocle_container: None,
|
||||
monocle_restore_idx: None,
|
||||
maximized_window: None,
|
||||
maximized_window_restore_idx: None,
|
||||
monocle_container_restore_idx: None,
|
||||
floating_windows: Vec::default(),
|
||||
layout: Layout::BSP,
|
||||
layout_flip: None,
|
||||
@@ -77,6 +84,20 @@ impl Workspace {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.hide();
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
for window in container.windows_mut() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore(&mut self) -> Result<()> {
|
||||
@@ -87,14 +108,31 @@ impl Workspace {
|
||||
window.restore();
|
||||
|
||||
if idx == i {
|
||||
to_focus = Option::from(window);
|
||||
to_focus = Option::from(*window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.maximize();
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
for window in container.windows_mut() {
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
window.restore();
|
||||
}
|
||||
|
||||
// Do this here to make sure that an error doesn't stop the restoration of other windows
|
||||
// Maximised windows should always be drawn at the top of the Z order
|
||||
if let Some(window) = to_focus {
|
||||
window.focus()?;
|
||||
if self.maximized_window().is_none() {
|
||||
window.focus()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -110,7 +148,9 @@ impl Workspace {
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
if let Some(window) = container.focused_window_mut() {
|
||||
window.set_position(&adjusted_work_area, true)?;
|
||||
}
|
||||
};
|
||||
} else if let Some(window) = self.maximized_window_mut() {
|
||||
window.maximize();
|
||||
} else if !self.containers().is_empty() {
|
||||
let layouts = self.layout().calculate(
|
||||
&adjusted_work_area,
|
||||
@@ -223,8 +263,26 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||
for x in self.containers() {
|
||||
if x.contains_window(hwnd) {
|
||||
for container in self.containers() {
|
||||
if container.contains_window(hwnd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
if hwnd == window.hwnd {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container() {
|
||||
if container.contains_window(hwnd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
if hwnd == window.hwnd {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -245,7 +303,7 @@ impl Workspace {
|
||||
|
||||
pub fn add_container(&mut self, container: Container) {
|
||||
self.containers_mut().push_back(container);
|
||||
self.focus_container(self.containers().len() - 1);
|
||||
self.focus_last_container();
|
||||
}
|
||||
|
||||
fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||
@@ -300,9 +358,7 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
if container_idx != 0 {
|
||||
self.focus_container(container_idx - 1);
|
||||
}
|
||||
self.focus_previous_container();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -310,10 +366,7 @@ impl Workspace {
|
||||
pub fn remove_focused_container(&mut self) -> Option<Container> {
|
||||
let focused_idx = self.focused_container_idx();
|
||||
let container = self.remove_container_by_idx(focused_idx);
|
||||
|
||||
if focused_idx != 0 {
|
||||
self.focus_container(focused_idx - 1);
|
||||
}
|
||||
self.focus_previous_container();
|
||||
|
||||
container
|
||||
}
|
||||
@@ -416,7 +469,11 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn new_container_for_window(&mut self, window: Window) {
|
||||
let next_idx = self.focused_container_idx() + 1;
|
||||
let next_idx = if self.containers().is_empty() {
|
||||
0
|
||||
} else {
|
||||
self.focused_container_idx() + 1
|
||||
};
|
||||
|
||||
let mut container = Container::default();
|
||||
container.add_window(window);
|
||||
@@ -497,11 +554,8 @@ impl Workspace {
|
||||
// it had before
|
||||
|
||||
self.set_monocle_container(Option::from(container));
|
||||
self.set_monocle_restore_idx(Option::from(focused_idx));
|
||||
|
||||
if focused_idx != 0 {
|
||||
self.focus_container(focused_idx - 1);
|
||||
}
|
||||
self.set_monocle_container_restore_idx(Option::from(focused_idx));
|
||||
self.focus_previous_container();
|
||||
|
||||
self.monocle_container_mut()
|
||||
.as_mut()
|
||||
@@ -513,7 +567,7 @@ impl Workspace {
|
||||
|
||||
pub fn reintegrate_monocle_container(&mut self) -> Result<()> {
|
||||
let restore_idx = self
|
||||
.monocle_restore_idx()
|
||||
.monocle_container_restore_idx()
|
||||
.context("there is no monocle restore index")?;
|
||||
|
||||
let container = self
|
||||
@@ -534,6 +588,69 @@ impl Workspace {
|
||||
.load_focused_window();
|
||||
|
||||
self.set_monocle_container(None);
|
||||
self.set_monocle_container_restore_idx(None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new_maximized_window(&mut self) -> Result<()> {
|
||||
let focused_idx = self.focused_container_idx();
|
||||
|
||||
let container = self
|
||||
.focused_container_mut()
|
||||
.context("there is no container")?;
|
||||
|
||||
let window = container
|
||||
.remove_focused_window()
|
||||
.context("there is no window")?;
|
||||
|
||||
if container.windows().is_empty() {
|
||||
self.containers_mut().remove(focused_idx);
|
||||
self.resize_dimensions_mut().remove(focused_idx);
|
||||
} else {
|
||||
container.load_focused_window();
|
||||
}
|
||||
|
||||
self.set_maximized_window(Option::from(window));
|
||||
self.set_maximized_window_restore_idx(Option::from(focused_idx));
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.maximize();
|
||||
}
|
||||
|
||||
self.focus_previous_container();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reintegrate_maximized_window(&mut self) -> Result<()> {
|
||||
let restore_idx = self
|
||||
.maximized_window_restore_idx()
|
||||
.context("there is no monocle restore index")?;
|
||||
|
||||
let window = self
|
||||
.maximized_window()
|
||||
.as_ref()
|
||||
.context("there is no monocle container")?;
|
||||
|
||||
let window = *window;
|
||||
if !self.containers().is_empty() && restore_idx > self.containers().len() - 1 {
|
||||
self.containers_mut()
|
||||
.resize(restore_idx, Container::default());
|
||||
}
|
||||
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(window);
|
||||
self.containers_mut().insert(restore_idx, container);
|
||||
|
||||
self.focus_container(restore_idx);
|
||||
|
||||
self.focused_container_mut()
|
||||
.context("there is no container")?
|
||||
.load_focused_window();
|
||||
|
||||
self.set_maximized_window(None);
|
||||
self.set_maximized_window_restore_idx(None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -551,7 +668,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn remove_focused_floating_window(&mut self) -> Option<Window> {
|
||||
let hwnd = WindowsApi::top_visible_window().ok()?;
|
||||
let hwnd = WindowsApi::foreground_window().ok()?;
|
||||
|
||||
let mut idx = None;
|
||||
for (i, window) in self.floating_windows.iter().enumerate() {
|
||||
@@ -572,6 +689,15 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visible_windows(&self) -> Vec<Option<&Window>> {
|
||||
let mut vec = vec![];
|
||||
for container in self.containers() {
|
||||
vec.push(container.focused_window());
|
||||
}
|
||||
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn visible_windows_mut(&mut self) -> Vec<Option<&mut Window>> {
|
||||
let mut vec = vec![];
|
||||
for container in self.containers_mut() {
|
||||
@@ -580,4 +706,16 @@ impl Workspace {
|
||||
|
||||
vec
|
||||
}
|
||||
|
||||
fn focus_previous_container(&mut self) {
|
||||
let focused_idx = self.focused_container_idx();
|
||||
|
||||
if focused_idx != 0 {
|
||||
self.focus_container(focused_idx - 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_last_container(&mut self) {
|
||||
self.focus_container(self.containers().len() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
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"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -12,8 +17,9 @@ komorebi-core = { path = "../komorebi-core" }
|
||||
clap = "3.0.0-beta.4"
|
||||
color-eyre = "0.5"
|
||||
dirs = "3"
|
||||
fs-tail = "0.1"
|
||||
paste = "1"
|
||||
powershell_script = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uds_windows = "1"
|
||||
uds_windows = "1"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
@@ -11,6 +14,7 @@ use clap::ArgEnum;
|
||||
use clap::Clap;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use fs_tail::TailedFile;
|
||||
use paste::paste;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
@@ -21,8 +25,8 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::Flip;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::LayoutFlip;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Sizing;
|
||||
use komorebi_core::SocketMessage;
|
||||
@@ -62,7 +66,8 @@ gen_enum_subcommand_args! {
|
||||
Move: OperationDirection,
|
||||
Stack: OperationDirection,
|
||||
CycleStack: CycleDirection,
|
||||
FlipLayout: LayoutFlip,
|
||||
FlipLayout: Flip,
|
||||
SetLayout: Layout,
|
||||
WatchConfiguration: BooleanState,
|
||||
FocusFollowsMouse: BooleanState
|
||||
}
|
||||
@@ -164,8 +169,19 @@ struct ApplicationTarget {
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(version = "0.1.0", author = "Jade Iqbal <jadeiqbal@fastmail.com>")]
|
||||
#[clap(setting = AppSettings::DeriveDisplayOrder)]
|
||||
struct WorkspaceRule {
|
||||
#[clap(arg_enum)]
|
||||
identifier: ApplicationIdentifier,
|
||||
/// Identifier as a string
|
||||
id: String,
|
||||
/// Monitor index (zero-indexed)
|
||||
monitor: usize,
|
||||
/// Workspace index on the specified monitor (zero-indexed)
|
||||
workspace: usize,
|
||||
}
|
||||
|
||||
#[derive(Clap)]
|
||||
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
|
||||
struct Opts {
|
||||
#[clap(subcommand)]
|
||||
subcmd: SubCommand,
|
||||
@@ -179,6 +195,8 @@ enum SubCommand {
|
||||
Stop,
|
||||
/// Show a JSON representation of the current window manager state
|
||||
State,
|
||||
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
Log,
|
||||
/// Change focus to the window in the specified direction
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
Focus(Focus),
|
||||
@@ -216,6 +234,8 @@ enum SubCommand {
|
||||
/// Adjust workspace padding on the focused workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
AdjustWorkspacePadding(PaddingAdjustment),
|
||||
/// Set the layout on the focused workspace
|
||||
ChangeLayout(SetLayout),
|
||||
/// Flip the layout on the focused workspace (BSP only)
|
||||
FlipLayout(FlipLayout),
|
||||
/// Promote the focused window to the top of the tree
|
||||
@@ -248,8 +268,14 @@ enum SubCommand {
|
||||
ToggleFloat,
|
||||
/// Toggle monocle mode for the focused container
|
||||
ToggleMonocle,
|
||||
/// Toggle native maximization for the focused window
|
||||
ToggleMaximize,
|
||||
/// Restore all hidden windows (debugging command)
|
||||
RestoreWindows,
|
||||
/// Force komorebi to manage the focused window
|
||||
Manage,
|
||||
/// Unmanage a window that was forcibly managed
|
||||
Unmanage,
|
||||
/// Reload ~/komorebi.ahk (if it exists)
|
||||
ReloadConfiguration,
|
||||
/// Toggle the automatic reloading of ~/komorebi.ahk (if it exists)
|
||||
@@ -258,6 +284,12 @@ enum SubCommand {
|
||||
/// Add a rule to always float the specified application
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
FloatRule(ApplicationTarget),
|
||||
/// Add a rule to always manage the specified application
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
ManageRule(ApplicationTarget),
|
||||
/// Add a rule to associate an application with a workspace
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
WorkspaceRule(WorkspaceRule),
|
||||
/// Identify an application that closes to the system tray
|
||||
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
|
||||
IdentifyTrayApplication(ApplicationTarget),
|
||||
@@ -274,10 +306,20 @@ pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
Ok(stream.write_all(&*bytes)?)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn main() -> Result<()> {
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
match opts.subcmd {
|
||||
SubCommand::Log => {
|
||||
let mut color_log = std::env::temp_dir();
|
||||
color_log.push("komorebi.log");
|
||||
let file = TailedFile::new(File::open(color_log)?);
|
||||
let locked = file.lock();
|
||||
for line in locked.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
}
|
||||
SubCommand::Focus(arg) => {
|
||||
send_message(&*SocketMessage::FocusWindow(arg.operation_direction).as_bytes()?)?;
|
||||
}
|
||||
@@ -330,6 +372,9 @@ fn main() -> Result<()> {
|
||||
SubCommand::ToggleMonocle => {
|
||||
send_message(&*SocketMessage::ToggleMonocle.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ToggleMaximize => {
|
||||
send_message(&*SocketMessage::ToggleMaximize.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::WorkspaceLayout(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::WorkspaceLayout(arg.monitor, arg.workspace, arg.value)
|
||||
@@ -383,17 +428,18 @@ fn main() -> Result<()> {
|
||||
SubCommand::Stop => {
|
||||
send_message(&*SocketMessage::Stop.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::FloatRule(arg) => match arg.identifier {
|
||||
ApplicationIdentifier::Exe => {
|
||||
send_message(&*SocketMessage::FloatExe(arg.id).as_bytes()?)?;
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
send_message(&*SocketMessage::FloatClass(arg.id).as_bytes()?)?;
|
||||
}
|
||||
ApplicationIdentifier::Title => {
|
||||
send_message(&*SocketMessage::FloatTitle(arg.id).as_bytes()?)?;
|
||||
}
|
||||
},
|
||||
SubCommand::FloatRule(arg) => {
|
||||
send_message(&*SocketMessage::FloatRule(arg.identifier, arg.id).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ManageRule(arg) => {
|
||||
send_message(&*SocketMessage::ManageRule(arg.identifier, arg.id).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::WorkspaceRule(arg) => {
|
||||
send_message(
|
||||
&*SocketMessage::WorkspaceRule(arg.identifier, arg.id, arg.monitor, arg.workspace)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::Stack(arg) => {
|
||||
send_message(&*SocketMessage::StackWindow(arg.operation_direction).as_bytes()?)?;
|
||||
}
|
||||
@@ -403,8 +449,11 @@ fn main() -> Result<()> {
|
||||
SubCommand::CycleStack(arg) => {
|
||||
send_message(&*SocketMessage::CycleStack(arg.cycle_direction).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ChangeLayout(arg) => {
|
||||
send_message(&*SocketMessage::ChangeLayout(arg.layout).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::FlipLayout(arg) => {
|
||||
send_message(&*SocketMessage::FlipLayout(arg.layout_flip).as_bytes()?)?;
|
||||
send_message(&*SocketMessage::FlipLayout(arg.flip).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::FocusMonitor(arg) => {
|
||||
send_message(&*SocketMessage::FocusMonitorNumber(arg.target).as_bytes()?)?;
|
||||
@@ -451,13 +500,13 @@ fn main() -> Result<()> {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?)
|
||||
println!("{}", line?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error)
|
||||
panic!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,6 +549,12 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::Manage => {
|
||||
send_message(&*SocketMessage::ManageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Unmanage => {
|
||||
send_message(&*SocketMessage::UnmanageFocusedWindow.as_bytes()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user