Compare commits

...

31 Commits

Author SHA1 Message Date
LGUG2Z
cab8b4ad52 chore(release): v0.1.3 2021-08-24 08:28:44 -07:00
LGUG2Z
05777c34b9 fix(wm): ensure removal of max + monocle windows
Previously, the implementation of maximized and monocle windows assumed
that the only valid state for them to transition to would be to restore
them to the index that they were maximized/monocle-d from in their host
workspace.

This is not exclusively the case as it is also possible for them to be
closed when they are in a maximized or monocle state.

This commit updates the Workspace.remove_window() fn to also look for
the hwnd to be removed in the monocle container and maximized window, if
they exist.

fix #19
2021-08-24 07:21:13 -07:00
LGUG2Z
5094001862 feat(wm): add send-to-workspace/monitor cmds
This commit adds two commands to allow the user to send the currently
focused container to a different workspace or monitor as a background
operation, without following the moved container to the destination
workspace or monitor.

resolve #20
2021-08-24 06:52:56 -07:00
LGUG2Z
bc08e177a1 fix(komorebic): add missing help annotations 2021-08-23 15:14:16 -07:00
LGUG2Z
87fe718754 feat(wm): add toggle-focus-follows-mouse cmd
Decided there should be a quick way to toggle the native ffm
functionality, it gets especially annoying when trying to click drop
downs from the system tray etc.

re #7
2021-08-23 14:08:40 -07:00
LGUG2Z
fb4fe4d9c3 refactor(derive-ahk): enforce no_implicit_prelude
Starting to implement the feedback I got from this post on Reddit
https://old.reddit.com/r/rust/comments/pa2997/code_review_request_first_derive_macro/.
2021-08-23 11:16:58 -07:00
LGUG2Z
b61b03b1c9 refactor(eyre): handle options with combinators
This commit removes the unnecessary eyre dependency and instead uses the
relevant imports from color-eyre.

Additionally, after reading the eyre readme a little more closely, I
have switched out .compat() for the ok_or() combinator function as
suggested here: https://github.com/yaahc/eyre#compatibility-with-anyhow.
2021-08-23 09:52:13 -07:00
LGUG2Z
a02cd699a0 refactor(derive-ahk): push up generation logic
This commit pushes as much of the generation logic as possible to the
derive-ahk crate, so that when it is used in komorebic, we only need to
do an as_bytes() call to prepare it for being written to a file.

Besides that, this commit changes the generation command name to
'ahk-library' for clarity, and adds both additional samples and
instructions in the readme file and Scoop post-install hook.
2021-08-23 07:49:37 -07:00
LGUG2Z
2c876701d8 feat(ahk): add cmd to generate helper lib
Woke up today and thought this would be a cool way to learn more about
deriving functionality with proc macros.

Hopefully having this wrapper/helper library will make first time
configuration for new users easier.
2021-08-22 18:54:44 -07:00
LGUG2Z
c42739591f build(windows-rs): upgrade to 0.19.0 2021-08-22 07:19:34 -07:00
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
32 changed files with 2063 additions and 639 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,9 @@ 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"
- Write-Host "`nRun 'cp $original_dir\komorebi.sample.ahk $Env:UserProfile\komorebi.ahk' to get started with the sample configuration"
- Write-Host "`nRun 'komorebic ahk-library' if you would like to generate an AHK helper library to use in your configuration"
- Write-Host "`nOnce you have a configuration file in place, you can run 'komorebic start' to start the window manager"

225
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"
@@ -226,6 +257,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "derive-ahk"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dirs"
version = "3.0.2"
@@ -270,10 +310,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 +469,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"
@@ -425,9 +489,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "0.4.7"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "kernel32-sys"
@@ -441,7 +505,7 @@ dependencies = [
[[package]]
name = "komorebi"
version = "0.1.0"
version = "0.1.3"
dependencies = [
"bindings",
"bitflags",
@@ -450,12 +514,12 @@ dependencies = [
"crossbeam-utils",
"ctrlc",
"dirs",
"eyre",
"getset",
"hotwatch",
"komorebi-core",
"lazy_static",
"nanoid",
"parking_lot",
"paste",
"serde",
"serde_json",
@@ -466,11 +530,12 @@ dependencies = [
"tracing-subscriber",
"uds_windows",
"which",
"winvd",
]
[[package]]
name = "komorebi-core"
version = "0.1.0"
version = "0.1.3"
dependencies = [
"bindings",
"clap",
@@ -482,12 +547,15 @@ dependencies = [
[[package]]
name = "komorebic"
version = "0.1.0"
version = "0.1.3"
dependencies = [
"bindings",
"clap",
"color-eyre",
"derive-ahk",
"dirs",
"fs-tail",
"heck",
"komorebi-core",
"paste",
"powershell_script",
@@ -510,9 +578,18 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.99"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5"
[[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"
@@ -534,9 +611,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 +768,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 +793,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 +999,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 +1021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
"redox_syscall 0.2.10",
]
[[package]]
@@ -965,18 +1086,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.127"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
checksum = "1056a0db1978e9dbf0f6e4fca677f6f9143dc1c19de346f22cac23e422196834"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.127"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
checksum = "13af2fbb8b60a8950d6c72a56d2095c28870367cc8e10c55e9745bac4995a2c4"
dependencies = [
"proc-macro2",
"quote",
@@ -1044,9 +1165,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",
@@ -1055,9 +1176,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.20.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0af066e6272f2175c1783cfc2ebf3e2d8dfe2c182b00677fdeccbf8291af83fb"
checksum = "4786f320388e6031aa78c68455553f4e3425deeeb40565fecba3d101c1faf21f"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
@@ -1096,6 +1217,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 +1283,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 +1323,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",
@@ -1324,9 +1456,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68088239696c06152844eadc03d262f088932cce50c67e4ace86e19d95e976fe"
checksum = "ef84dd25f4c69a271b1bba394532bf400523b43169de21dfc715e8f8e491053d"
dependencies = [
"const-sha1",
"windows_gen",
@@ -1335,18 +1467,47 @@ dependencies = [
[[package]]
name = "windows_gen"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf583322dc423ee021035b358e535015f7fd163058a31e2d37b99a939141121d"
checksum = "ac7bb21b8ff5e801232b72a6ff554b4cc0cef9ed9238188c3ca78fe3968a7e5d"
dependencies = [
"windows_quote",
"windows_reader",
]
[[package]]
name = "windows_macros"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58acfb8832e9f707f8997bd161e537a1c1f603e60a5bd9c3cf53484fdcc998f3"
checksum = "5566b8c51118769e4a9094a688bf1233a3f36aacbfc78f3b15817fe0b6e0442f"
dependencies = [
"syn",
"windows_gen",
"windows_quote",
"windows_reader",
]
[[package]]
name = "windows_quote"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4af8236a9493c38855f95cdd11b38b342512a5df4ee7473cffa828b5ebb0e39c"
[[package]]
name = "windows_reader"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8d5cf83fb08083438c5c46723e6206b2970da57ce314f80b57724439aaacab"
[[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]]

View File

@@ -2,6 +2,7 @@
members = [
"bindings",
"derive-ahk",
"komorebi",
"komorebi-core",
"komorebic"

257
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,66 @@ 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
send-to-monitor Send the focused window to the specified monitor
send-to-workspace Send the focused window to the specified workspace
focus-monitor Focus the specified monitor
focus-workspace Focus the specified workspace on the focused monitor
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 Enable or disable watching 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
toggle-focus-follows-mouse Toggle focus follows mouse for the operating system
ahk-library Generate a library of AutoHotKey helper functions
help Print this message or the help of the given subcommand(s)
```
### AutoHotKey Helper Library for `komorebic`
Additionally, you may run `komorebic.exe ahk-library` to
generate [a helper library for AutoHotKey](komorebic.lib.sample.ahk) which wraps every `komorebic` command in a native
AHK function.
If you include the generated library at the top of your `~/komorebi.ahk` configuration file, you will be able to call
any of the functions that it contains. A sample AHK script that shows how this library can be
used [is available here](komorebi.sample.with.lib.ahk).
## Features
- [x] Multi-monitor
@@ -141,8 +238,10 @@ each command.
- [x] Cycle through stacked windows
- [x] Change focused window by direction
- [x] Move focused window container in direction
- [x] Move focused window container to monitor
- [x] Move focused window container to workspace
- [x] Move focused window container to monitor and follow
- [x] Move focused window container to workspace follow
- [x] Send focused window container to monitor
- [x] Send focused window container to workspace
- [x] Mouse follows focused container
- [x] Resize window container in direction
- [ ] Resize child window containers by split ratio
@@ -152,18 +251,20 @@ 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
- [x] Load configuration on startup
- [x] Manually reload configuration
- [x] Watch configuration for changes
- [x] Helper library for AutoHotKey
- [x] View window manager state
## Development
@@ -184,7 +285,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 +301,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

@@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
windows = "0.18"
windows = "0.19"
[build-dependencies]
windows = "0.18"
windows = "0.19"

14
derive-ahk/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "derive-ahk"
version = "0.1.0"
edition = "2018"
[lib]
proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0"
syn = "1.0"
quote = "1.0"

120
derive-ahk/src/lib.rs Normal file
View File

@@ -0,0 +1,120 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![no_implicit_prelude]
use ::std::clone::Clone;
use ::std::convert::From;
use ::std::convert::Into;
use ::std::iter::Extend;
use ::std::iter::Iterator;
use ::std::matches;
use ::std::string::ToString;
use ::std::unreachable;
use ::quote::quote;
use ::std::option::Option::Some;
use ::syn::parse_macro_input;
use ::syn::Data;
use ::syn::DataEnum;
use ::syn::DeriveInput;
use ::syn::Fields;
use ::syn::FieldsNamed;
use ::syn::FieldsUnnamed;
#[proc_macro_derive(AhkFunction)]
pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Struct(s) => match s.fields {
Fields::Named(FieldsNamed { named, .. }) => {
let idents = named.iter().map(|f| &f.ident);
let arguments = quote! {#(#idents), *}.to_string();
let idents = named.iter().map(|f| &f.ident);
let called_arguments = quote! {#(%#idents%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
Run, komorebic.exe {} {}, , Hide
}}"#,
::std::stringify!(#name),
#arguments,
stringify!(#name).to_kebab_case(),
#called_arguments
)
}
}
}
}
_ => unreachable!("only to be used on structs with named fields"),
},
_ => unreachable!("only to be used on structs"),
}
.into()
}
#[proc_macro_derive(AhkLibrary)]
pub fn ahk_library(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Enum(DataEnum { variants, .. }) => {
let enums = variants.iter().filter(|&v| {
matches!(v.fields, Fields::Unit) || matches!(v.fields, Fields::Unnamed(..))
});
let mut stream = ::proc_macro2::TokenStream::new();
for variant in enums.clone() {
match &variant.fields {
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
for field in unnamed {
stream.extend(quote! {
v.push(#field::generate_ahk_function());
});
}
}
Fields::Unit => {
let name = &variant.ident;
stream.extend(quote! {
v.push(::std::format!(r#"
{}() {{
Run, komorebic.exe {}, , Hide
}}"#,
::std::stringify!(#name),
::std::stringify!(#name).to_kebab_case()
));
});
}
Fields::Named(_) => {
unreachable!("only to be used with unnamed and unit fields");
}
}
}
quote! {
impl #name {
fn generate_ahk_library() -> String {
let mut v: Vec<String> = vec![String::from("; Generated by komorebic.exe")];
#stream
v.join("\n")
}
}
}
}
_ => unreachable!("only to be used on enums"),
}
.into()
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.0"
version = "0.1.3"
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;
@@ -29,14 +32,19 @@ pub enum SocketMessage {
CycleStack(CycleDirection),
MoveContainerToMonitorNumber(usize),
MoveContainerToWorkspaceNumber(usize),
SendContainerToMonitorNumber(usize),
SendContainerToWorkspaceNumber(usize),
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,12 +62,13 @@ 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),
ToggleFocusFollowsMouse,
}
impl SocketMessage {
@@ -96,7 +105,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

@@ -0,0 +1,223 @@
#SingleInstance Force
#Include %A_ScriptDir%\komorebic.lib.ahk
; Enable hot reloading of changes to this file
WatchConfiguration("enable")
; Ensure there are 5 workspaces created on monitor 0
EnsureWorkspaces(0, 5)
; Configure the 1st workspace
WorkspaceName(0, 0, "bsp")
; Configure the 2nd workspace
WorkspaceName(0, 1, "columns") ; Optionally set the name of the workspace
WorkspacePadding(0, 1, 30) ; Set the padding around the edge of the screen
ContainerPadding(0, 1, 30) ; Set the padding between the containers on the screen
WorkspaceRule("exe", "slack.exe", 0, 1) ; Always show chat apps on this workspace
; Configure the 3rd workspace
WorkspaceName(0, 2, "thicc")
WorkspacePadding(0, 2, 200) ; Set some super thicc padding
; Configure the 4th workspace
WorkspaceName(0, 3, "matrix")
WorkspacePadding(0, 3, 0) ; No padding at all
ContainerPadding(0, 3, 0) ; Matrix-y hacker vibes
; Configure the 5th workspace
WorkspaceName(0, 4, "floaty")
WorkspaceTiling(0, 4, "disable") ; Everything floats here
; Configure floating rules
FloatRule("class", "SunAwtDialog") ; All the IntelliJ popups
FloatRule("title", "Control Panek")
FloatRule("class", "TaskManagerWindow")
FloatRule("exe", "Wally.exe")
FloatRule("exe", "wincompose.exe")
FloatRule("exe", "1Password.exe")
FloatRule("exe", "Wox.exe")
; Identify Minimize-to-Tray Applications
IdentifyTrayApplication("exe", "Discord.exe")
; Change the focused window, Alt + Vim direction keys
!h::
Focus("left")
return
!j::
Focus("down")
return
!k::
Focus("up")
return
!l::
Focus("right")
return
; Move the focused window in a given direction, Alt + Shift + Vim direction keys
!+h::
Move("left")
return
!+j::
Move("down")
return
!+k::
Move("up")
return
!+l::
Move("right")
return
; Stack the focused window in a given direction, Alt + Shift + direction keys
!+Left::
Stack("left")
return
!+Down::
Stack("down")
return
!+Up::
Stack("up")
return
!+Right::
Stack("right")
return
!]::
CycleStack("next")
return
![::
CycleStack("previous")
return
; Unstack the focused window, Alt + Shift + D
!+d::
Unstack()
return
; Promote the focused window to the top of the tree, Alt + Shift + Enter
!+Enter::
Promote()
return
; Manage the focused window
!=::
Manage()
return
; Unmanage the focused window
!-::
Unmanage()
return
; Switch to an equal-width, max-height column layout on the main workspace, Alt + Shift + C
!+c::
ChangeLayout("columns")
return
; Switch to the default bsp tiling layout on the main workspace, Alt + Shift + T
!+t::
ChangeLayout("bsp")
return
; Toggle the Monocle layout for the focused window, Alt + Shift + F
!+f::
ToggleMonocle()
return
; Toggle native maximize for the focused window, Alt + Shift + =
!+=::
ToggleMaximize()
return
; Flip horizontally, Alt + X
!x::
FlipLayout("horizontal")
return
; Flip vertically, Alt + Y
!y::
FlipLayout("vertical")
return
; Force a retile if things get janky, Alt + Shift + R
!+r::
Retile()
return
; Float the focused window, Alt + T
!t::
ToggleFloat()
return
; Reload ~/komorebi.ahk, Alt + O
!o::
ReloadConfiguration()
return
; Pause responding to any window events or komorebic commands, Alt + P
!p::
TogglePause()
return
; Toggle focus follows mouse
!0::
ToggleFocusFollowsMouse()
return
; Switch to workspace
!1::
Send !
FocusWorkspace(0)
return
!2::
Send !
FocusWorkspace(1)
return
!3::
Send !
FocusWorkspace(2)
return
!4::
Send !
FocusWorkspace(3)
return
!5::
Send !
FocusWorkspace(4)
return
; Move window to workspace
!+1::
MoveToWorkspace(0)
return
!+2::
MoveToWorkspace(1)
return
!+3::
MoveToWorkspace(2)
return
!+4::
MoveToWorkspace(3)
return
!+5::
MoveToWorkspace(4)
return

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.0"
version = "0.1.3"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -15,11 +15,11 @@ crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
ctrlc = "3"
dirs = "3"
eyre = "0.6"
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 +29,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::eyre::anyhow;
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");
}
@@ -73,9 +82,11 @@ fn setup() -> Result<WorkerGuard> {
std::env::set_var("RUST_LOG", "info");
}
let home = dirs::home_dir().context("there is no home directory")?;
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let 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,11 +129,11 @@ fn setup() -> Result<WorkerGuard> {
}
}));
Ok(guard)
Ok((guard, color_guard))
}
pub fn load_configuration() -> Result<()> {
let home = dirs::home_dir().context("there is no home directory")?;
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut config_v1 = home.clone();
config_v1.push("komorebi.ahk");
@@ -131,7 +147,7 @@ pub fn load_configuration() -> Result<()> {
config_v1
.as_os_str()
.to_str()
.context("cannot convert path to string")?
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("autohotkey.exe")
@@ -143,7 +159,7 @@ pub fn load_configuration() -> Result<()> {
config_v2
.as_os_str()
.to_str()
.context("cannot convert path to string")?
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
Command::new("AutoHotkey64.exe")
@@ -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

@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use getset::CopyGetters;
use getset::Getters;
@@ -56,7 +56,7 @@ impl Monitor {
pub fn add_container(&mut self, container: Container) -> Result<()> {
let workspace = self
.focused_workspace_mut()
.context("there is no workspace")?;
.ok_or_else(|| anyhow!("there is no workspace"))?;
workspace.add_container(container);
@@ -70,16 +70,25 @@ 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")?
.ok_or_else(|| anyhow!("there is no workspace"))?;
if workspace.maximized_window().is_some() {
return Err(anyhow!(
"cannot move native maximized window to another monitor or workspace"
));
}
let container = workspace
.remove_focused_container()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let workspaces = self.workspaces_mut();
@@ -120,7 +129,7 @@ impl Monitor {
if name.is_some() {
self.workspaces_mut()
.get_mut(idx)
.context("there is no workspace")?
.ok_or_else(|| anyhow!("there is no workspace"))?
.set_name(name);
}
}
@@ -136,7 +145,7 @@ impl Monitor {
let work_area = *self.work_area_size();
self.focused_workspace_mut()
.context("there is no workspace")?
.ok_or_else(|| anyhow!("there is no workspace"))?
.update(&work_area)?;
Ok(())

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::eyre::anyhow;
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) => {
@@ -102,6 +106,12 @@ impl WindowManager {
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, true)?;
}
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, false)?;
}
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, false)?;
}
SocketMessage::TogglePause => {
tracing::info!("pausing");
self.is_paused = !self.is_paused;
@@ -118,7 +128,7 @@ impl WindowManager {
let work_area = *monitor.work_area_size();
let workspace = monitor
.focused_workspace_mut()
.context("there is no workspace")?;
.ok_or_else(|| anyhow!("there is no workspace"))?;
// Reset any resize adjustments if we want to force a retile
for resize in workspace.resize_dimensions_mut() {
@@ -157,7 +167,8 @@ impl WindowManager {
}
SocketMessage::State => {
let state = serde_json::to_string_pretty(&window_manager::State::from(self))?;
let mut socket = dirs::home_dir().context("there is no home directory")?;
let mut socket =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
socket.push("komorebic.sock");
let socket = socket.as_path();
@@ -174,6 +185,13 @@ impl WindowManager {
WindowsApi::disable_focus_follows_mouse()?;
}
}
SocketMessage::ToggleFocusFollowsMouse => {
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
} else {
WindowsApi::enable_focus_follows_mouse()?;
}
}
SocketMessage::ReloadConfiguration => {
Self::reload_configuration();
}
@@ -182,19 +200,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::eyre::anyhow;
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)
@@ -55,7 +57,7 @@ impl WindowManager {
| WindowManagerEvent::MoveResizeEnd(_, window) => {
let monitor_idx = self
.monitor_idx_from_window(*window)
.context("there is no monitor associated with this window, it may have already been destroyed")?;
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
self.focus_monitor(monitor_idx)?;
}
@@ -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")?
.ok_or_else(|| anyhow!("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)?;
}
@@ -189,7 +210,7 @@ impl WindowManager {
let old_position = *workspace
.latest_layout()
.get(focused_idx)
.context("there is no latest layout")?;
.ok_or_else(|| anyhow!("there is no latest layout"))?;
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
// See Window.set_position() in window.rs for comments
@@ -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() {
@@ -279,7 +305,8 @@ impl WindowManager {
}
}
let mut hwnd_json = dirs::home_dir().context("there is no home directory")?;
let mut hwnd_json =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
hwnd_json.push("komorebi.hwnd.json");
let file = OpenOptions::new()
.write(true)

View File

@@ -2,7 +2,7 @@ use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use serde::ser::SerializeStruct;
use serde::Serialize;
@@ -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());
@@ -161,12 +187,12 @@ impl Window {
pub fn style(self) -> Result<GwlStyle> {
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
GwlStyle::from_bits(bits).context("there is no gwl style")
GwlStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
}
pub fn ex_style(self) -> Result<GwlExStyle> {
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
GwlExStyle::from_bits(bits).context("there is no gwl style")
GwlExStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
}
pub fn title(self) -> Result<String> {
@@ -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,20 +3,21 @@ 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::anyhow;
use color_eyre::eyre::ContextCompat;
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 +29,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 +45,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 +65,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().ok_or_else(|| anyhow!("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");
@@ -121,7 +149,7 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn watch_configuration(&mut self, enable: bool) -> Result<()> {
let home = dirs::home_dir().context("there is no home directory")?;
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
let mut config_v1 = home.clone();
config_v1.push("komorebi.ahk");
@@ -146,7 +174,7 @@ impl WindowManager {
config
.as_os_str()
.to_str()
.context("cannot convert path to string")?
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
// Always make absolutely sure that there isn't an already existing watch, because
// hotwatch allows multiple watches to be registered for the same path
@@ -177,7 +205,7 @@ impl WindowManager {
config
.as_os_str()
.to_str()
.context("cannot convert path to string")?
.ok_or_else(|| anyhow!("cannot convert path to string"))?
);
self.hotwatch.unwatch(config)?;
@@ -187,16 +215,167 @@ 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)
.ok_or_else(|| anyhow!("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)
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.workspaces_mut()
.get_mut(op.origin_workspace_idx)
.ok_or_else(|| anyhow!("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)
.ok_or_else(|| anyhow!("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)
.ok_or_else(|| anyhow!("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");
self.focused_monitor_mut()
.context("there is no monitor")?
.ok_or_else(|| anyhow!("there is no monitor"))?
.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 {
@@ -207,7 +386,7 @@ impl WindowManager {
// attach to the thread of the desktop window always seems to result in "Access is
// denied (os error 5)"
WindowsApi::set_foreground_window(desktop_window.hwnd())
.map_err(|error| eyre::anyhow!("{} {}:{}", error, file!(), line!()))?;
.map_err(|error| anyhow!("{} {}:{}", error, file!(), line!()))?;
}
}
@@ -230,7 +409,7 @@ impl WindowManager {
let focused_idx_resize = workspace
.resize_dimensions()
.get(focused_idx)
.context("there is no resize adjustment for this container")?;
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
if direction.is_valid(
workspace.layout(),
@@ -254,28 +433,28 @@ 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(),
}
}
let resize = workspace.layout().resize(
unaltered
.get(focused_idx)
.context("there is no last layout")?,
.ok_or_else(|| anyhow!("there is no last layout"))?,
focused_idx_resize,
direction,
sizing,
@@ -309,17 +488,27 @@ impl WindowManager {
pub fn move_container_to_monitor(&mut self, idx: usize, follow: bool) -> Result<()> {
tracing::info!("moving container");
let monitor = self.focused_monitor_mut().context("there is no monitor")?;
let container = monitor
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.focused_workspace_mut()
.context("there is no workspace")?
.ok_or_else(|| anyhow!("there is no workspace"))?;
if workspace.maximized_window().is_some() {
return Err(anyhow!(
"cannot move native maximized window to another monitor or workspace"
));
}
let container = workspace
.remove_focused_container()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let target_monitor = self
.monitors_mut()
.get_mut(idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.add_container(container)?;
target_monitor.load_focused_workspace()?;
@@ -335,9 +524,13 @@ impl WindowManager {
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
tracing::info!("moving container");
let monitor = self.focused_monitor_mut().context("there is no monitor")?;
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
monitor.move_container_to_workspace(idx, follow)?;
monitor.load_focused_workspace()?;
self.update_focused_workspace(true)
}
@@ -348,7 +541,7 @@ impl WindowManager {
let new_idx = workspace
.new_idx_for_direction(direction)
.context("this is not a valid direction from the current position")?;
.ok_or_else(|| anyhow!("this is not a valid direction from the current position"))?;
workspace.focus_container(new_idx);
self.focused_window_mut()?.focus()?;
@@ -365,7 +558,7 @@ impl WindowManager {
let current_idx = workspace.focused_container_idx();
let new_idx = workspace
.new_idx_for_direction(direction)
.context("this is not a valid direction from the current position")?;
.ok_or_else(|| anyhow!("this is not a valid direction from the current position"))?;
workspace.swap_containers(current_idx, new_idx);
workspace.focus_container(new_idx);
@@ -379,7 +572,7 @@ impl WindowManager {
let container = self.focused_container_mut()?;
if container.windows().len() == 1 {
return Err(eyre::anyhow!("there is only one window in this container"));
return Err(anyhow!("there is only one window in this container"));
}
let current_idx = container.focused_window_idx();
@@ -406,9 +599,9 @@ impl WindowManager {
);
if is_valid {
let new_idx = workspace
.new_idx_for_direction(direction)
.context("this is not a valid direction from the current position")?;
let new_idx = workspace.new_idx_for_direction(direction).ok_or_else(|| {
anyhow!("this is not a valid direction from the current position")
})?;
let adjusted_new_index = if new_idx > current_container_idx {
new_idx - 1
@@ -437,7 +630,7 @@ impl WindowManager {
tracing::info!("removing window");
if self.focused_container()?.windows().len() == 1 {
return Err(eyre::anyhow!("a container must have at least one window"));
return Err(anyhow!("a container must have at least one window"));
}
let workspace = self.focused_workspace_mut()?;
@@ -455,7 +648,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 +661,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))]
@@ -487,19 +680,9 @@ impl WindowManager {
let window = workspace
.floating_windows_mut()
.last_mut()
.context("there is no floating window")?;
.ok_or_else(|| anyhow!("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 +725,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 +765,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),
},
};
}
@@ -601,7 +812,7 @@ impl WindowManager {
let padding = workspace
.workspace_padding()
.context("there is no workspace padding")?;
.ok_or_else(|| anyhow!("there is no workspace padding"))?;
workspace.set_workspace_padding(Option::from(sizing.adjust_by(padding, adjustment)));
@@ -616,7 +827,7 @@ impl WindowManager {
let padding = workspace
.container_padding()
.context("there is no container padding")?;
.ok_or_else(|| anyhow!("there is no container padding"))?;
workspace.set_container_padding(Option::from(sizing.adjust_by(padding, adjustment)));
@@ -633,12 +844,12 @@ impl WindowManager {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_tile(tile);
@@ -659,7 +870,7 @@ impl WindowManager {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let focused_workspace_idx = monitor.focused_workspace_idx();
@@ -667,7 +878,7 @@ impl WindowManager {
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_layout(layout);
@@ -691,7 +902,7 @@ impl WindowManager {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
monitor.ensure_workspace_count(workspace_count);
@@ -710,12 +921,12 @@ impl WindowManager {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_workspace_padding(Option::from(size));
@@ -734,12 +945,12 @@ impl WindowManager {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_name(Option::from(name.clone()));
monitor.workspace_names_mut().insert(workspace_idx, name);
@@ -759,12 +970,12 @@ impl WindowManager {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.context("there is no monitor")?;
.ok_or_else(|| anyhow!("there is no monitor"))?;
workspace.set_container_padding(Option::from(size));
@@ -774,7 +985,7 @@ impl WindowManager {
pub fn focused_monitor_work_area(&self) -> Result<Rect> {
Ok(*self
.focused_monitor()
.context("there is no monitor")?
.ok_or_else(|| anyhow!("there is no monitor"))?
.work_area_size())
}
@@ -785,7 +996,7 @@ impl WindowManager {
if self.monitors().get(idx).is_some() {
self.monitors.focus(idx);
} else {
return Err(eyre::anyhow!("this is not a valid monitor index"));
return Err(anyhow!("this is not a valid monitor index"));
}
Ok(())
@@ -805,16 +1016,16 @@ impl WindowManager {
pub fn focused_workspace(&self) -> Result<&Workspace> {
self.focused_monitor()
.context("there is no monitor")?
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace()
.context("there is no workspace")
.ok_or_else(|| anyhow!("there is no workspace"))
}
pub fn focused_workspace_mut(&mut self) -> Result<&mut Workspace> {
self.focused_monitor_mut()
.context("there is no monitor")?
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_mut()
.context("there is no workspace")
.ok_or_else(|| anyhow!("there is no workspace"))
}
#[tracing::instrument(skip(self))]
@@ -823,7 +1034,7 @@ impl WindowManager {
let monitor = self
.focused_monitor_mut()
.context("there is no workspace")?;
.ok_or_else(|| anyhow!("there is no workspace"))?;
monitor.focus_workspace(idx)?;
monitor.load_focused_workspace()?;
@@ -837,7 +1048,7 @@ impl WindowManager {
let monitor = self
.focused_monitor_mut()
.context("there is no workspace")?;
.ok_or_else(|| anyhow!("there is no workspace"))?;
monitor.focus_workspace(monitor.new_workspace_idx())?;
monitor.load_focused_workspace()?;
@@ -848,18 +1059,18 @@ impl WindowManager {
pub fn focused_container(&self) -> Result<&Container> {
self.focused_workspace()?
.focused_container()
.context("there is no container")
.ok_or_else(|| anyhow!("there is no container"))
}
pub fn focused_container_mut(&mut self) -> Result<&mut Container> {
self.focused_workspace_mut()?
.focused_container_mut()
.context("there is no container")
.ok_or_else(|| anyhow!("there is no container"))
}
fn focused_window_mut(&mut self) -> Result<&mut Window> {
self.focused_container_mut()?
.focused_window_mut()
.context("there is no window")
.ok_or_else(|| anyhow!("there is no window"))
}
}

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

@@ -3,9 +3,9 @@ use std::convert::TryFrom;
use std::convert::TryInto;
use std::ffi::c_void;
use color_eyre::eyre::ContextCompat;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::Error;
use color_eyre::Result;
use eyre::Error;
use bindings::Windows::Win32::Foundation::BOOL;
use bindings::Windows::Win32::Foundation::HANDLE;
@@ -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;
@@ -66,8 +67,10 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
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 +262,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 +286,7 @@ impl WindowsApi {
}
}
#[allow(dead_code)]
pub fn top_window() -> Result<isize> {
Result::from(WindowsResult::from(unsafe { GetTopWindow(HWND::NULL).0 }))
}
@@ -283,12 +295,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;
@@ -301,7 +315,7 @@ impl WindowsApi {
next_hwnd = Self::next_window(HWND(next_hwnd))?;
}
Err(eyre::anyhow!("could not find next window"))
Err(anyhow!("could not find next window"))
}
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
@@ -450,7 +464,7 @@ impl WindowsApi {
Ok(Self::exe_path(handle)?
.split('\\')
.last()
.context("there is no last element")?
.ok_or_else(|| anyhow!("there is no last element"))?
.to_string())
}
@@ -544,6 +558,19 @@ impl WindowsApi {
}))
}
pub fn focus_follows_mouse() -> Result<bool> {
let mut is_enabled: BOOL = unsafe { std::mem::zeroed() };
Self::system_parameters_info_w(
SPI_GETACTIVEWINDOWTRACKING,
0,
(&mut is_enabled as *mut BOOL).cast(),
SPIF_SENDCHANGE,
)?;
Ok(is_enabled.into())
}
pub fn enable_focus_follows_mouse() -> Result<()> {
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,

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

@@ -1,6 +1,7 @@
use std::collections::VecDeque;
use std::num::NonZeroUsize;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use getset::CopyGetters;
@@ -9,8 +10,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 +29,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 +63,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 +85,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 +109,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 +149,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,
@@ -189,16 +230,16 @@ impl Workspace {
pub fn focus_container_by_window(&mut self, hwnd: isize) -> Result<()> {
let container_idx = self
.container_idx_for_window(hwnd)
.context("there is no container/window")?;
.ok_or_else(|| anyhow!("there is no container/window"))?;
let container = self
.containers_mut()
.get_mut(container_idx)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window_idx = container
.idx_for_window(hwnd)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
container.focus_window(window_idx);
self.focus_container(container_idx);
@@ -223,8 +264,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;
}
}
@@ -235,7 +294,7 @@ impl Workspace {
pub fn promote_container(&mut self) -> Result<()> {
let container = self
.remove_focused_container()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
self.containers_mut().push_front(container);
self.resize_dimensions_mut().insert(0, None);
self.focus_container(0);
@@ -245,7 +304,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> {
@@ -270,29 +329,56 @@ impl Workspace {
return Ok(());
}
if let Some(container) = self.monocle_container_mut() {
if let Some(window_idx) = container
.windows()
.iter()
.position(|window| window.hwnd == hwnd)
{
container
.remove_window_by_idx(window_idx)
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.set_monocle_container(None);
self.set_monocle_container_restore_idx(None);
}
return Ok(());
}
}
if let Some(window) = self.maximized_window() {
if window.hwnd == hwnd {
self.set_maximized_window(None);
self.set_maximized_window_restore_idx(None);
return Ok(());
}
}
let container_idx = self
.container_idx_for_window(hwnd)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
let container = self
.containers_mut()
.get_mut(container_idx)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window_idx = container
.windows()
.iter()
.position(|window| window.hwnd == hwnd)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
container
.remove_window_by_idx(window_idx)
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.containers_mut()
.remove(container_idx)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
// Whenever a container is empty, we need to remove any resize dimensions for it too
if self.resize_dimensions().get(container_idx).is_some() {
@@ -300,9 +386,7 @@ impl Workspace {
}
}
if container_idx != 0 {
self.focus_container(container_idx - 1);
}
self.focus_previous_container();
Ok(())
}
@@ -310,10 +394,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
}
@@ -340,11 +421,11 @@ impl Workspace {
let container = self
.focused_container_mut()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
// This is a little messy
let adjusted_target_container_index = if container.windows().is_empty() {
@@ -364,13 +445,13 @@ impl Workspace {
let target_container = self
.containers_mut()
.get_mut(adjusted_target_container_index)
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
target_container.add_window(window);
self.focus_container(adjusted_target_container_index);
self.focused_container_mut()
.context("there is no container")?
.ok_or_else(|| anyhow!("there is no container"))?
.load_focused_window();
Ok(())
@@ -381,11 +462,11 @@ impl Workspace {
let container = self
.focused_container_mut()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.containers_mut().remove(focused_container_idx);
@@ -405,7 +486,7 @@ impl Workspace {
let focused_idx = self.focused_container_idx();
let window = self
.remove_focused_floating_window()
.context("there is no floating window")?;
.ok_or_else(|| anyhow!("there is no floating window"))?;
let mut container = Container::default();
container.add_window(window);
@@ -416,7 +497,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);
@@ -441,11 +526,11 @@ impl Workspace {
let container = self
.focused_container_mut()
.context("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.context("there is no window")?;
.ok_or_else(|| anyhow!("there is no window"))?;
if container.windows().is_empty() {
self.containers_mut().remove(focused_idx);
@@ -490,22 +575,19 @@ impl Workspace {
let container = self
.containers_mut()
.remove(focused_idx)
.context("there is not container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
// We don't remove any resize adjustments for a monocle, because when this container is
// inevitably reintegrated, it would be weird if it doesn't go back to the dimensions
// 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()
.context("there is no monocle container")?
.ok_or_else(|| anyhow!("there is no monocle container"))?
.load_focused_window();
Ok(())
@@ -513,13 +595,13 @@ impl Workspace {
pub fn reintegrate_monocle_container(&mut self) -> Result<()> {
let restore_idx = self
.monocle_restore_idx()
.context("there is no monocle restore index")?;
.monocle_container_restore_idx()
.ok_or_else(|| anyhow!("there is no monocle restore index"))?;
let container = self
.monocle_container_mut()
.as_ref()
.context("there is no monocle container")?;
.ok_or_else(|| anyhow!("there is no monocle container"))?;
let container = container.clone();
if restore_idx > self.containers().len() - 1 {
@@ -530,10 +612,73 @@ impl Workspace {
self.containers_mut().insert(restore_idx, container);
self.focus_container(restore_idx);
self.focused_container_mut()
.context("there is no container")?
.ok_or_else(|| anyhow!("there is no container"))?
.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()
.ok_or_else(|| anyhow!("there is no container"))?;
let window = container
.remove_focused_window()
.ok_or_else(|| anyhow!("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()
.ok_or_else(|| anyhow!("there is no monocle restore index"))?;
let window = self
.maximized_window()
.as_ref()
.ok_or_else(|| anyhow!("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()
.ok_or_else(|| anyhow!("there is no container"))?
.load_focused_window();
self.set_maximized_window(None);
self.set_maximized_window_restore_idx(None);
Ok(())
}
@@ -551,7 +696,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 +717,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 +734,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);
}
}

185
komorebic.lib.sample.ahk Normal file
View File

@@ -0,0 +1,185 @@
; Generated by komorebic.exe
Start() {
Run, komorebic.exe start, , Hide
}
Stop() {
Run, komorebic.exe stop, , Hide
}
State() {
Run, komorebic.exe state, , Hide
}
Log() {
Run, komorebic.exe log, , Hide
}
Focus(operation_direction) {
Run, komorebic.exe focus %operation_direction%, , Hide
}
Move(operation_direction) {
Run, komorebic.exe move %operation_direction%, , Hide
}
Stack(operation_direction) {
Run, komorebic.exe stack %operation_direction%, , Hide
}
Resize(edge, sizing) {
Run, komorebic.exe resize %edge% %sizing%, , Hide
}
Unstack() {
Run, komorebic.exe unstack, , Hide
}
CycleStack(cycle_direction) {
Run, komorebic.exe cycle-stack %cycle_direction%, , Hide
}
MoveToMonitor(target) {
Run, komorebic.exe move-to-monitor %target%, , Hide
}
MoveToWorkspace(target) {
Run, komorebic.exe move-to-workspace %target%, , Hide
}
SendToMonitor(target) {
Run, komorebic.exe send-to-monitor %target%, , Hide
}
SendToWorkspace(target) {
Run, komorebic.exe send-to-workspace %target%, , Hide
}
FocusMonitor(target) {
Run, komorebic.exe focus-monitor %target%, , Hide
}
FocusWorkspace(target) {
Run, komorebic.exe focus-workspace %target%, , Hide
}
NewWorkspace() {
Run, komorebic.exe new-workspace, , Hide
}
AdjustContainerPadding(sizing, adjustment) {
Run, komorebic.exe adjust-container-padding %sizing% %adjustment%, , Hide
}
AdjustWorkspacePadding(sizing, adjustment) {
Run, komorebic.exe adjust-workspace-padding %sizing% %adjustment%, , Hide
}
ChangeLayout(layout) {
Run, komorebic.exe change-layout %layout%, , Hide
}
FlipLayout(flip) {
Run, komorebic.exe flip-layout %flip%, , Hide
}
Promote() {
Run, komorebic.exe promote, , Hide
}
Retile() {
Run, komorebic.exe retile, , Hide
}
EnsureWorkspaces(monitor, workspace_count) {
Run, komorebic.exe ensure-workspaces %monitor% %workspace_count%, , Hide
}
ContainerPadding(monitor, workspace, size) {
Run, komorebic.exe container-padding %monitor% %workspace% %size%, , Hide
}
WorkspacePadding(monitor, workspace, size) {
Run, komorebic.exe workspace-padding %monitor% %workspace% %size%, , Hide
}
WorkspaceLayout(monitor, workspace, value) {
Run, komorebic.exe workspace-layout %monitor% %workspace% %value%, , Hide
}
WorkspaceTiling(monitor, workspace, value) {
Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide
}
WorkspaceName(monitor, workspace, value) {
Run, komorebic.exe workspace-name %monitor% %workspace% %value%, , Hide
}
TogglePause() {
Run, komorebic.exe toggle-pause, , Hide
}
ToggleTiling() {
Run, komorebic.exe toggle-tiling, , Hide
}
ToggleFloat() {
Run, komorebic.exe toggle-float, , Hide
}
ToggleMonocle() {
Run, komorebic.exe toggle-monocle, , Hide
}
ToggleMaximize() {
Run, komorebic.exe toggle-maximize, , Hide
}
RestoreWindows() {
Run, komorebic.exe restore-windows, , Hide
}
Manage() {
Run, komorebic.exe manage, , Hide
}
Unmanage() {
Run, komorebic.exe unmanage, , Hide
}
ReloadConfiguration() {
Run, komorebic.exe reload-configuration, , Hide
}
WatchConfiguration(boolean_state) {
Run, komorebic.exe watch-configuration %boolean_state%, , Hide
}
FloatRule(identifier, id) {
Run, komorebic.exe float-rule %identifier% %id%, , Hide
}
ManageRule(identifier, id) {
Run, komorebic.exe manage-rule %identifier% %id%, , Hide
}
WorkspaceRule(identifier, id, monitor, workspace) {
Run, komorebic.exe workspace-rule %identifier% %id% %monitor% %workspace%, , Hide
}
IdentifyTrayApplication(identifier, id) {
Run, komorebic.exe identify-tray-application %identifier% %id%, , Hide
}
FocusFollowsMouse(boolean_state) {
Run, komorebic.exe focus-follows-mouse %boolean_state%, , Hide
}
ToggleFocusFollowsMouse() {
Run, komorebic.exe toggle-focus-follows-mouse, , Hide
}
AhkLibrary() {
Run, komorebic.exe ahk-library, , Hide
}

View File

@@ -1,19 +1,27 @@
[package]
name = "komorebic"
version = "0.1.0"
version = "0.1.3"
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
[dependencies]
bindings = { package = "bindings", path = "../bindings" }
derive-ahk = { path = "../derive-ahk" }
komorebi-core = { path = "../komorebi-core" }
clap = "3.0.0-beta.4"
color-eyre = "0.5"
dirs = "3"
fs-tail = "0.1"
heck = "0.3"
paste = "1"
powershell_script = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uds_windows = "1"
uds_windows = "1"

View File

@@ -1,16 +1,23 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::ErrorKind;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::stringify;
use clap::AppSettings;
use clap::ArgEnum;
use clap::Clap;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use fs_tail::TailedFile;
use heck::KebabCase;
use paste::paste;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
@@ -19,14 +26,24 @@ use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
use derive_ahk::AhkFunction;
use derive_ahk::AhkLibrary;
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;
trait AhkLibrary {
fn generate_ahk_library() -> String;
}
trait AhkFunction {
fn generate_ahk_function() -> String;
}
#[derive(ArgEnum)]
enum BooleanState {
Enable,
@@ -47,7 +64,7 @@ macro_rules! gen_enum_subcommand_args {
( $( $name:ident: $element:ty ),+ ) => {
$(
paste! {
#[derive(clap::Clap)]
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
#[clap(arg_enum)]
[<$element:snake>]: $element
@@ -62,7 +79,8 @@ gen_enum_subcommand_args! {
Move: OperationDirection,
Stack: OperationDirection,
CycleStack: CycleDirection,
FlipLayout: LayoutFlip,
FlipLayout: Flip,
ChangeLayout: Layout,
WatchConfiguration: BooleanState,
FocusFollowsMouse: BooleanState
}
@@ -71,7 +89,7 @@ macro_rules! gen_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
$(
#[derive(clap::Clap)]
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
/// Target index (zero-indexed)
target: usize,
@@ -83,6 +101,8 @@ macro_rules! gen_target_subcommand_args {
gen_target_subcommand_args! {
MoveToMonitor,
MoveToWorkspace,
SendToMonitor,
SendToWorkspace,
FocusMonitor,
FocusWorkspace
}
@@ -95,7 +115,7 @@ macro_rules! gen_workspace_subcommand_args {
( $( $name:ident: $(#[enum] $(@$arg_enum:tt)?)? $value:ty ),+ ) => (
paste! {
$(
#[derive(clap::Clap)]
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct [<Workspace $name>] {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -121,7 +141,7 @@ gen_workspace_subcommand_args! {
Tiling: #[enum] BooleanState
}
#[derive(Clap)]
#[derive(Clap, AhkFunction)]
struct Resize {
#[clap(arg_enum)]
edge: OperationDirection,
@@ -129,7 +149,7 @@ struct Resize {
sizing: Sizing,
}
#[derive(Clap)]
#[derive(Clap, AhkFunction)]
struct EnsureWorkspaces {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -137,41 +157,89 @@ struct EnsureWorkspaces {
workspace_count: usize,
}
#[derive(Clap)]
struct Padding {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
/// Pixels to pad with as an integer
size: i32,
macro_rules! gen_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
$(
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index on the specified monitor (zero-indexed)
workspace: usize,
/// Pixels to pad with as an integer
size: i32,
}
)+
};
}
#[derive(Clap)]
struct PaddingAdjustment {
#[clap(arg_enum)]
sizing: Sizing,
/// Pixels to adjust by as an integer
adjustment: i32,
gen_padding_subcommand_args! {
ContainerPadding,
WorkspacePadding
}
#[derive(Clap)]
struct ApplicationTarget {
macro_rules! gen_padding_adjustment_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
$(
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
#[clap(arg_enum)]
sizing: Sizing,
/// Pixels to adjust by as an integer
adjustment: i32,
}
)+
};
}
gen_padding_adjustment_subcommand_args! {
AdjustContainerPadding,
AdjustWorkspacePadding
}
macro_rules! gen_application_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
$(
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
#[clap(arg_enum)]
identifier: ApplicationIdentifier,
/// Identifier as a string
id: String,
}
)+
};
}
gen_application_target_subcommand_args! {
FloatRule,
ManageRule,
IdentifyTrayApplication
}
#[derive(Clap, AhkFunction)]
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(version = "0.1.0", author = "Jade Iqbal <jadeiqbal@fastmail.com>")]
#[clap(setting = AppSettings::DeriveDisplayOrder)]
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
struct Opts {
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Clap)]
#[derive(Clap, AhkLibrary)]
enum SubCommand {
/// Start komorebi.exe as a background process
Start,
@@ -179,6 +247,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),
@@ -202,6 +272,12 @@ enum SubCommand {
/// Move the focused window to the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
MoveToWorkspace(MoveToWorkspace),
/// Send the focused window to the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
SendToMonitor(SendToMonitor),
/// Send the focused window to the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
SendToWorkspace(SendToWorkspace),
/// Focus the specified monitor
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FocusMonitor(FocusMonitor),
@@ -212,11 +288,15 @@ enum SubCommand {
NewWorkspace,
/// Adjust container padding on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
AdjustContainerPadding(PaddingAdjustment),
AdjustContainerPadding(AdjustContainerPadding),
/// Adjust workspace padding on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
AdjustWorkspacePadding(PaddingAdjustment),
AdjustWorkspacePadding(AdjustWorkspacePadding),
/// Set the layout on the focused workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ChangeLayout(ChangeLayout),
/// Flip the layout on the focused workspace (BSP only)
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FlipLayout(FlipLayout),
/// Promote the focused window to the top of the tree
Promote,
@@ -227,10 +307,10 @@ enum SubCommand {
EnsureWorkspaces(EnsureWorkspaces),
/// Set the container padding for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ContainerPadding(Padding),
ContainerPadding(ContainerPadding),
/// Set the workspace padding for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspacePadding(Padding),
WorkspacePadding(WorkspacePadding),
/// Set the layout for the specified workspace
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WorkspaceLayout(WorkspaceLayout),
@@ -248,21 +328,38 @@ 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)
/// Enable or disable watching of ~/komorebi.ahk (if it exists)
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
WatchConfiguration(WatchConfiguration),
/// Add a rule to always float the specified application
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FloatRule(ApplicationTarget),
FloatRule(FloatRule),
/// Add a rule to always manage the specified application
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ManageRule(ManageRule),
/// 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),
IdentifyTrayApplication(IdentifyTrayApplication),
/// Enable or disable focus follows mouse for the operating system
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FocusFollowsMouse(FocusFollowsMouse),
/// Toggle focus follows mouse for the operating system
ToggleFocusFollowsMouse,
/// Generate a library of AutoHotKey helper functions
AhkLibrary,
}
pub fn send_message(bytes: &[u8]) -> Result<()> {
@@ -274,10 +371,44 @@ 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::AhkLibrary => {
let mut library = dirs::home_dir().context("there is no home directory")?;
library.push("komorebic.lib.ahk");
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(library.clone())?;
file.write_all(SubCommand::generate_ahk_library().as_bytes())?;
println!(
"\nAHK helper library for komorebic written to {}",
library
.to_str()
.context("could not find the path to the generated ahk lib file")?
);
println!(
"\nYou can include the library at the top of your ~/komorebi.ahk config with this line:"
);
println!("\n#Include %A_ScriptDir%\\komorebic.lib.ahk");
}
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()?)?;
}
@@ -299,6 +430,12 @@ fn main() -> Result<()> {
SubCommand::MoveToWorkspace(arg) => {
send_message(&*SocketMessage::MoveContainerToWorkspaceNumber(arg.target).as_bytes()?)?;
}
SubCommand::SendToMonitor(arg) => {
send_message(&*SocketMessage::SendContainerToMonitorNumber(arg.target).as_bytes()?)?;
}
SubCommand::SendToWorkspace(arg) => {
send_message(&*SocketMessage::SendContainerToWorkspaceNumber(arg.target).as_bytes()?)?;
}
SubCommand::ContainerPadding(arg) => {
send_message(
&*SocketMessage::ContainerPadding(arg.monitor, arg.workspace, arg.size)
@@ -321,6 +458,9 @@ fn main() -> Result<()> {
&*SocketMessage::AdjustContainerPadding(arg.sizing, arg.adjustment).as_bytes()?,
)?;
}
SubCommand::ToggleFocusFollowsMouse => {
send_message(&*SocketMessage::ToggleFocusFollowsMouse.as_bytes()?)?;
}
SubCommand::ToggleTiling => {
send_message(&*SocketMessage::ToggleTiling.as_bytes()?)?;
}
@@ -330,6 +470,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 +526,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 +547,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 +598,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 +647,12 @@ fn main() -> Result<()> {
.as_bytes()?,
)?;
}
SubCommand::Manage => {
send_message(&*SocketMessage::ManageFocusedWindow.as_bytes()?)?;
}
SubCommand::Unmanage => {
send_message(&*SocketMessage::UnmanageFocusedWindow.as_bytes()?)?;
}
}
Ok(())