Compare commits

...

21 Commits

Author SHA1 Message Date
LGUG2Z
381253da20 fix(wm): switch to correct ws when following links
Silly boolean error meant that if a link was clicked in one
monitor/workspace, the browser, if not on the same workspace, would be
brought into the workspace the link was clicked in.

This was because I was checking if the focused monitor != the known
monitor && the focused workspace != the known workspace, when in fact,
we don't need both of those conditions to be true in order to switch to
where the browser is, we only need one of them to be true.

After changing the && (and) to a || (or), the behaviour is now as
expected, and clicking a link will switch to the workspace where the
browser is open.
2021-08-20 17:16:16 -07:00
LGUG2Z
cf7532330b chore(release): prepare v0.1.1 2021-08-20 14:00:06 -07:00
LGUG2Z
ffb86458f0 build(cargo): set -Ctarget-feature=+crt-static
I tried running prebuilt binaries from GitHub Actions on a fresh Windows
11 VM and was faced with the error "The code execution cannot proceed
because VCRUNTIME140.dll was not found."

After Googling around a little I think this may help with that issue;
definitely don't want to have to troubleshoot this for non-developers
who just want to use a tiling window manager.
2021-08-20 13:47:48 -07:00
LGUG2Z
292bdb282f refactor(clippy): apply all super pedantic lints
Realised that I hadn't turned on super pedantic mode for clippy in the
komorebi-core and komorebic crates. This commit ensures the same clippy
config across all crates and applies the lint suggestions that arose as
a result of turning on the same config everywhere.
2021-08-20 13:26:16 -07:00
LGUG2Z
1625ca6e5d feat(wm): allow all app identifiers for all rules
This commit removes the restriction on adding title rules for tray
applications and forcibly managed applications since there wasn't any
good reason for disallowing them.

Also updated the sample config and the readme to add a section for
common first time tips and to remove the big JSON blob showing an
outdated example of output from the state command.
2021-08-20 12:49:37 -07:00
LGUG2Z
df07409a2f refactor(workspace): extract fns for container focusing 2021-08-20 12:01:11 -07:00
LGUG2Z
2e86b607b2 refactor(wm): improve data consistency + scoping
Just a little bit of clean up to make sure that the float rule data
structures match the same emerging pattern as the data structures for
other kinds of rules.

Also some refactoring of Window.should_manage to ensure stricter scoping
where locks are gained on global static variables.
2021-08-19 17:18:24 -07:00
LGUG2Z
6f7e87799b fix(wm): handle winvd errors gracefully
Of course, the crate built to interact with an undocumented COM API is
not the best candidate for unwrap and expect calls...

fix #15
2021-08-19 14:52:04 -07:00
LGUG2Z
4e9b294835 feat(wm): add additional manage rules
Following on from 8ffe6f78b7, this commit
introduces a command to add rules to forcibly manage windows that don't
get picked up by the rough heuristics that are able to target most
windows for management in Window.should_manage.

Since there is again no overlap (or at least, no undesired overlap)
between executable names and classes, I'll keep both class and exe names
in a single lookup vec.

re #16
2021-08-19 14:35:02 -07:00
LGUG2Z
8ffe6f78b7 feat(wm): forcibly manage and unmanage windows
Added commands to forcibly manage and unmanage windows if they don't get
picked up for tiling automatically. This commit adds support for running
those operations on the currently focused window, but if there is a need
to specify a hwnd to operate on, that could be added pretty easily too
in the future, though I'd like to keep the complexity of looking up and
passing hwnds to a command out of the CLI if possible.

This commit also fixes an issue with restoring floating windows. I'm not
sure what happened, but at some point, for me at least,
WindowsApi::top_visible_window started returning explorer.exe all the
time, so I've switched this out for WindowsApi::foreground_window.

I have a feeling I was using TopWindow before, thinking it was
GetForegroundWindow, which it isn't, and it wasn't reliable, so I
created the top_visible_window abstraction on top of it, which also
turned out to be unreliable. Anyway, it's working now.

I think the next step will be to create a manage-rule command to
compliment the float-rule command which users can use to handle edge
cases with their apps in their configuration.

re #16
2021-08-19 13:31:49 -07:00
LGUG2Z
42b9305dfe refactor(windows_callbacks): push logic further up
The win_event_hook was still just a messy copy-paste from the yatta
days.

This commit pushes the logic around deciding if we should emit a
WindowManagerEvent::Show from a WinEvent::ObjectNameChange up to the
WindowManagerEvent::from_win_event method.

Now, the win_event_hook is just calling other functions that decide what
to do with the window and passing on the results.
2021-08-19 09:25:10 -07:00
LGUG2Z
1eba8aa01d feat(wm): add workspace rules
This feature allows users to specify which monitor/workspace an
application's window, identified either by executable name or window
class name, should be assigned to.

A new fn, WindowManager.enforce_workspace_rules, is called whenever a
new rule is added, and periodically whenever an event is processed by
komorebi (just after orphan windows are repead, before the matching and
processing of the specific event).

Both class and exe identifiers are stored in the same HashMap for the
sake of simplicity, as I couldn't think of any situations where there
might be a clash between the two identifiers.

Did some light refactoring of window_manager.rs to make the new()
constructor a static method on the WindowManager struct.

Also fixed a bug in Workspace.new_container_for_window where the focused
index was not getting set correctly when the workspace had no
containers.
2021-08-19 08:19:34 -07:00
LGUG2Z
74811fbe13 fix(wm): limit to a single virtual desktop
An issue was reported in which switching between Windows Virtual
Desktops was causing issues with the layout generation. This was due to
WinEvents being emitted from other Virtual Desktops ending up in the WM
state when they shouldn't.

This commit introduces a check to ensure that the WM will only listen to
events and commands emitted from the Windows Virtual Desktop that it was
started on.

fix #15
2021-08-19 07:52:44 -07:00
LGUG2Z
209cd82892 fix(wm): prevent hidden_hwnds deadlock
I used a parking_lot to detect what I suspected to be the deadlock
resulting in issue #13.

I was pleasantly surprised by the alternative to std::sync::Mutex
provided by parking_lot, especially not having to unlock it to use it,
and of course the excellent and intuitive (even if experimental)
deadlock detector.

I have decided to use parking_lot::Mutex as an almost-drop-in
replacement for std::sync::Mutex, as I expect that this isn't the last
time komorebi will have a deadlocking issue, and I have put the deadlock
detection code which runs in a separate thread behind a
"deadlock_detection" feature.

The actual deadlock itself was solved by scoping the first lock in the
handler for WindowManagerEvent::Hide and then executing any required
operations (some of which, like window.maximize(), may require another
lock on HIDDEN_HWNDS) in a separate scope once the previous lock has
been dropped.

In the future I should look at integrating globals like HIDDEN_HWNDS
into WindowManager in a way that won't lead to double-mutable-borrow
issues.

fix #13
2021-08-19 06:31:02 -07:00
LGUG2Z
98f731ba13 feat(komorebic): add change-layout command
The handler for this was already hooked up in process_command.rs, but I
had forgotten to add the command to the cli.

resolve #14
2021-08-18 16:39:04 -07:00
LGUG2Z
c7bf09e34b fix(wm): restore focus to monocle on ws switch 2021-08-18 10:22:58 -07:00
LGUG2Z
0725549d45 feat(wm): add native window maximization toggle
Windows that have been maximized do not retain their maximized state
across workspaces as workspaces are built on top of sending SW_HIDE and
SW_SHOW events which at various points of the event loop end up
overriding SW_SHOWMAXIMIZED and SW_SHOWMAXIMIZE.

To handle this use case, I have added a new 'komorebic toggle-maximize'
command which sends SW_MAXIMIZE for a window and keeps a record of the
window in the focused workspace in the same way that monocle windows are
tracked.

In this way, komorebi can know when switching to a workspace if it has
to restore a window to a native maximized state.

Some additional edge cases are caught in this commit in showing and
hiding workspaces, to also account for floating windows and monocle
containers.

resolve #12
2021-08-18 09:49:05 -07:00
LGUG2Z
13b335cecc feat(komorebic): add log command
This commit adds a log command directly to the komorebic cli to make it
easier for users to check the logs if they don't have tail installed or
are not familiar with it.

A separate logfile with ANSI color codes is now being written to the
user's tempdir, which is tailed by the log command until the process is
halted by a Ctrl-C signal.
2021-08-18 06:21:19 -07:00
LGUG2Z
23aada05d0 refactor(komorebic): inject metadata from cargo 2021-08-17 14:15:13 -07:00
LGUG2Z
f11dcbc0cb ci(dependabot): add configuration file 2021-08-17 11:53:29 -07:00
LGUG2Z
564ee89c84 ci(scoop): stop proc if running before updates
For future upgrades of komorebi via Scoop, the proc will most likely be
running on the user's system. This commit adds a pre_install hook to run
'komorebi stop' if komorebi is running at the time of the upgrade.

resolve #11
2021-08-17 11:21:40 -07:00
26 changed files with 1164 additions and 495 deletions

21
.github/dependabot.yml vendored Normal file
View 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

View File

@@ -20,6 +20,8 @@ jobs:
build:
name: Build
runs-on: windows-latest
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
strategy:
fail-fast: false
matrix:

View File

@@ -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
View File

@@ -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
View File

@@ -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
}
```

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 => {

View File

@@ -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!(),
},

View File

@@ -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

View File

@@ -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

View File

@@ -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 = []

View File

@@ -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(()),

View File

@@ -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")?;

View File

@@ -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");

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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(&center, 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),
},
};
}

View File

@@ -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,
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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) => {

View File

@@ -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);
}
}

View File

@@ -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"

View File

@@ -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(())