Compare commits

..

33 Commits

Author SHA1 Message Date
LGUG2Z
355bcb6877 feat(windows): expand api wrappers 2021-09-16 10:55:13 -07:00
LGUG2Z
b8a27a93fe feat(ffm): explicitly require flag to enable
Following the discovery that the custom FFM implementation significantly
increases CPU usage, and that the underlying library used to track mouse
events is already as optimised as possible for CPU usage, this commit
makes the enabling of custom FFM explicit via a command line flag when
launching the window manager.

The underlying library does not provide for a way to clean up and
recreate a message loop on demand, which means that once it starts,
there is no way of reclaiming those CPU cycles even when FFM is
disabled.

If a user has not started komorebi with the --ffm flag and tries to
enable or toggle custom FFM, a warning will be shown in the logs and
komorebi will override their selection to operate on the Windows FFM
implementation.

In light of this, the default implementation values for komorebic's FFM
commands have been updated to 'windows'.

This commit also takes the opportunity to allow the state and stop
commands to pass when the window manager is in a paused state.

resolve #33
2021-09-16 09:42:13 -07:00
LGUG2Z
28a641609c fix(wm): update target monitor after send op
This commit ensures that the focused workspace on the target monitor is
updated with the latest layout after it receives a window via the
send-to-monitor command.

resolve #37
2021-09-15 07:18:10 -07:00
LGUG2Z
f1ee5ea194 feat(wm): make invisible borders configurable
Following the changes I witnessed in the invisible window border size
following an OS update, this commit makes the invisible border offset
configurable via a new komorebic command 'invisible-borders'.

When sending a new set of invisible border offset dimensions via
komorebic, a full retile across all monitors will take place after the
new values have been set.

The default values have been set to what is currently correct for my
machine, and will likely be updated again in the same way in the future
if further changes occur in subsequent OS updates.

This commit also updates some dependencies to their latest releases, and
removes from the CI workflow a line that attempts to delete the
rustup-init.exe binary after installation which has been causing builds
to fail.

resolve #35
2021-09-14 21:26:18 -07:00
LGUG2Z
ff53533da0 feat(wm): add cmd to id apps that overflow borders
Applications like Spotify and Discord draw over the default invisible
borders of Windows 10, which means that when komorebi is setting their
positions, the offset is always off by the amount of pixels of the
invisible borders on each side.

This commit makes it possible to identify applications that have
overflowing borders so that they can be handled appropriately by the
window manager.

This commit also takes the opportunity to consolidate the tray and multi
window identifiers into a single vector instead of spreading them across
multiple vectors by identifier type.

resolve #32
2021-09-13 09:03:40 -07:00
LGUG2Z
c4c8bd7d4b feat(wm): reconcile monitor state
When monitors turn on and off, they do not retain their hmonitor id,
therefore this commit introduces an initial attempt to reconcile invalid
and valid hmonitors after monitor changes based on the windows that are
assigned to them.

If a monitor has at least one window, and has been assigned a new
hmonitor id, komorebi will look up the current hmonitor of that window's
hwnd and update Monitor.id in-place.

When reconciling monitors, any monitor marked as invalid will be purged
from the window manager state.

This commit also applies some of the new clippy lints that come along
with the latest nightly release of Rust.

resolve #31
2021-09-13 07:24:09 -07:00
LGUG2Z
e1bd0e9fcb fix(ffm): handle multiple overlay window classes
Explorer windows are made up of multiple different sub-windows which can
make trouble for the ffm implementation if not known about.

This commit pushes up a check to ignore the raise request if the window
are the cursor position is already raised and in the foreground, and
also makes checks against an overlay_classes array to try and look up an
underlying hwnd that should probably be raised by a cursor move.
2021-09-07 08:38:41 -07:00
LGUG2Z
368d41e3e0 fix(ffm): raise when switching focus from anywhere
Previously, when switching focus from unmanaged windows such as the
taskbar and the system tray, komorebi would still recognise its last
known focused window as the currently focused window although that would
not be strictly true, making the ffm functionality stop working until
focus was set to a known window specifically either via a click or an
alt-tab.

This commit fixes that by always also comparing against what the OS has
registered as the current foreground window.

There is another little fix here to reset the pending raise op tracker
whenever ffm is toggled or disabled in the event that it ever gets
stuck.
2021-09-07 08:38:41 -07:00
LGUG2Z
2b7c51b87b refactor(ffm): add selection of ffm implementation
This commit adds an optional flag to allow users to select the focus
follows mouse implementation that they wish to use (komorebi or
windows). The flag defaults to komorebi.

The ahk-derive crate has been updated to enable the generation of
wrappers fns that require flags.

I pushed the ffm check up to listen_for_movements() so that we don't
even try to listen to the next event from the message loop unless
komorebi-flavoured ffm is enabled.

re #7
2021-09-07 08:38:41 -07:00
LGUG2Z
ce3c742e09 feat(ffm): add custom ffm/autoraise implementation
This commit implements an initial attempt at a custom focus follows
mouse and autoraise implementation which only responds to hwnds managed
by komorebi.

I was browsing GitHub and came across the winput crate which has a clean
API for tracking both mouse movements and button presses, which seems to
be just enough to get this functionality working.

Once again, Chromium and Electron are the bane of every platform they
run on and Windows is no exception, so I've had to add a hack to work
around the legacy Chrome windows that get drawn on top of Electron apps
with the Chrome_RenderWidgetHostHWND class.

It is fairly naive; it just looks up an alternative (and hopefully
correct) hwnd based on the exe name, but this will no doubt be fragile
when it comes to applications that have multiple windows spawned from
the same exe.

For now I've opted to keep the same komorebic commands for enabling,
disabling and toggling focus-follows-mouse, in order to preserve
backwards compat, but those commands will now enable and disable this
custom implementation instead of the native Windows X-Mouse
implementation.

Perhaps in the future the specific implementation to target could be
specified through the use of an optional flag.

re #7
2021-09-07 08:38:41 -07:00
LGUG2Z
2a4e6fa6da feat(wm): get monitor idx from cursor on ws switch
If a user wants to switch workspace on a secondary monitor which
contains no windows, komorebi doesn't register the monitor as focused
unless the corresponding komorebic command to switch monitor focus is
called explicitly. This generally works fine for users with a
keyboard-heavy workflow.

This commit changes the workspace switching behaviour to look up the
current monitor based on the cursor position just before the switch
takes place, so that the behaviour is still intuitive when trying to
change the monitor focus via the mouse.

re #30
2021-09-07 08:33:53 -07:00
LGUG2Z
2d19109fb6 feat(wm): allow direct querying of focused objects
This commit adds a new query command to komorebic, which allows for the
current focused monitor, workspace, container and window indices to be
queried directly without having to use jq run lookups on the entire
output of the state command.

resolve #24
2021-09-02 12:58:14 -07:00
LGUG2Z
a9a0ecd49d chore(deps): bump cc and object crates 2021-09-01 08:12:52 -07:00
dependabot[bot]
5ec2b80c3a chore(deps): bump proc-macro2 from 1.0.28 to 1.0.29
Bumps [proc-macro2](https://github.com/alexcrichton/proc-macro2) from 1.0.28 to 1.0.29.
- [Release notes](https://github.com/alexcrichton/proc-macro2/releases)
- [Commits](https://github.com/alexcrichton/proc-macro2/compare/1.0.28...1.0.29)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:34:00 -07:00
dependabot[bot]
4cc059ff1d chore(deps): bump sysinfo from 0.20.1 to 0.20.2
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.20.1 to 0.20.2.
- [Release notes](https://github.com/GuillaumeGomez/sysinfo/releases)
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:28:03 -07:00
dependabot[bot]
d4d7e2b694 chore(deps): bump parking_lot from 0.11.1 to 0.11.2
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.11.1 to 0.11.2.
- [Release notes](https://github.com/Amanieu/parking_lot/releases)
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/0.11.1...0.11.2)

---
updated-dependencies:
- dependency-name: parking_lot
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:25:07 -07:00
dependabot[bot]
84752c4338 chore(deps): bump serde_json from 1.0.66 to 1.0.67
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.66 to 1.0.67.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.66...v1.0.67)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:24:35 -07:00
dependabot[bot]
ffa0786178 chore(deps): bump serde from 1.0.129 to 1.0.130
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.129 to 1.0.130.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.129...v1.0.130)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 07:24:13 -07:00
LGUG2Z
752dde2288 docs(readme): add link to zh translation on wiki
Adding a section under ##About for links to translations of the document
in other languages, starting with @crosstyan's Chinese translation.

resolve #21
2021-08-29 07:30:25 -07:00
LGUG2Z
dc771a104d build(cargo): update dependencies 2021-08-26 09:58:18 -07:00
LGUG2Z
e5a7c140ff refactor(macros): add trailing commas 2021-08-26 09:53:15 -07:00
LGUG2Z
61b231be28 docs(readme): fix some bad copypasta 2021-08-24 11:05:33 -07:00
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
31 changed files with 2256 additions and 479 deletions

View File

@@ -66,7 +66,6 @@ jobs:
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
del rustup-init.exe
shell: powershell
- name: Ensure stable toolchain is up to date
run: rustup update stable

View File

@@ -55,5 +55,6 @@ scoop:
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"

149
Cargo.lock generated
View File

@@ -73,9 +73,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.69"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
[[package]]
name = "cfg-if"
@@ -257,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"
@@ -301,7 +310,7 @@ checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall 0.2.10",
"redox_syscall",
"winapi 0.3.9",
]
@@ -480,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"
@@ -496,16 +505,16 @@ dependencies = [
[[package]]
name = "komorebi"
version = "0.1.1"
version = "0.1.3"
dependencies = [
"bindings",
"bitflags",
"clap",
"color-eyre",
"crossbeam-channel",
"crossbeam-utils",
"ctrlc",
"dirs",
"eyre",
"getset",
"hotwatch",
"komorebi-core",
@@ -522,12 +531,13 @@ dependencies = [
"tracing-subscriber",
"uds_windows",
"which",
"winput",
"winvd",
]
[[package]]
name = "komorebi-core"
version = "0.1.1"
version = "0.1.3"
dependencies = [
"bindings",
"clap",
@@ -539,13 +549,15 @@ dependencies = [
[[package]]
name = "komorebic"
version = "0.1.1"
version = "0.1.3"
dependencies = [
"bindings",
"clap",
"color-eyre",
"derive-ahk",
"dirs",
"fs-tail",
"heck",
"komorebi-core",
"paste",
"powershell_script",
@@ -568,15 +580,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.99"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
[[package]]
name = "lock_api"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
dependencies = [
"scopeguard",
]
@@ -758,9 +770,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.26.1"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2766204889d09937d00bfbb7fec56bb2a199e2ade963cab19185d8a6104c7c"
checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2"
dependencies = [
"memchr",
]
@@ -785,9 +797,9 @@ checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55"
[[package]]
name = "parking_lot"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
@@ -796,16 +808,16 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"backtrace",
"cfg-if 1.0.0",
"instant",
"libc",
"petgraph",
"redox_syscall 0.2.10",
"redox_syscall",
"smallvec",
"thread-id",
"winapi 0.3.9",
@@ -871,9 +883,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.28"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
dependencies = [
"unicode-xid",
]
@@ -989,12 +1001,6 @@ 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"
@@ -1011,7 +1017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall 0.2.10",
"redox_syscall",
]
[[package]]
@@ -1049,9 +1055,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.20"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "ryu"
@@ -1076,18 +1082,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.127"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.127"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [
"proc-macro2",
"quote",
@@ -1096,9 +1102,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.66"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
"itoa",
"ryu",
@@ -1155,9 +1161,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.74"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
dependencies = [
"proc-macro2",
"quote",
@@ -1166,9 +1172,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.20.0"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0af066e6272f2175c1783cfc2ebf3e2d8dfe2c182b00677fdeccbf8291af83fb"
checksum = "92d77883450d697c0010e60db3d940ed130b0ed81d27485edee981621b434e52"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
@@ -1209,12 +1215,12 @@ dependencies = [
[[package]]
name = "thread-id"
version = "3.3.0"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f"
dependencies = [
"libc",
"redox_syscall 0.1.57",
"redox_syscall",
"winapi 0.3.9",
]
@@ -1239,9 +1245,9 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.26"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
checksum = "c2ba9ab62b7d6497a8638dfda5e5c4fb3b2d5a7fca4118f2b96151c8ef1a437e"
dependencies = [
"cfg-if 1.0.0",
"pin-project-lite",
@@ -1262,9 +1268,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.15"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2"
checksum = "98863d0dd09fa59a1b79c6750ad80dbda6b75f4e71c437a6a1a8cb91a8bcbd77"
dependencies = [
"proc-macro2",
"quote",
@@ -1273,9 +1279,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.19"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8"
checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf"
dependencies = [
"lazy_static",
]
@@ -1313,9 +1319,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.2.20"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cbe87a2fa7e35900ce5de20220a582a9483a7063811defce79d7cbd59d4cfe"
checksum = "62af966210b88ad5776ee3ba12d5f35b8d6a2b2a12168f3080cf02b814d7376b"
dependencies = [
"ansi_term",
"chrono",
@@ -1446,9 +1452,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",
@@ -1457,18 +1463,45 @@ 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 = "winput"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4bec39938e0ae68b300e2a4197b6437f13d53d1c146c6e297e346a71d5dde9"
dependencies = [
"winapi 0.3.9",
]
[[package]]

View File

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

143
README.md
View File

@@ -14,6 +14,10 @@ _komorebi_ allows you to control application windows, virtual workspaces and dis
used with third-party software such as [AutoHotKey](https://github.com/Lexikos/AutoHotkey_L) to set user-defined
keyboard shortcuts.
Translations of this document can be found in the project wiki:
- [komorebi 中文用户指南](https://github.com/LGUG2Z/komorebi/wiki/README-zh) (by [@crosstyan](https://github.com/crosstyan))
## Description
_komorebi_ only responds to [WinEvents](https://docs.microsoft.com/en-us/windows/win32/winauto/event-constants) and the
@@ -160,6 +164,26 @@ komorebic.exe identify-tray-application exe Discord.exe
# komorebic.exe identify-tray-application title [TITLE]
```
#### Focus Follows Mouse
`komorebi` supports two focus-follows-mouse implementations; the native Windows Xmouse implementation, which treats the
desktop, the task bar, and the system tray as windows and switches focus to them eagerly, and a custom `komorebi`
implementation, which only considers windows managed by `komorebi` as valid targets to switch focus to when moving the
mouse.
To enable the `komorebi` implementation you must start the process with the `--ffm` flag to explicitly enable the feature.
This is because the mouse tracking required for this feature significantly increases the CPU usage of the process (on my
machine, it jumps from <1% to ~4~), and this CPU increase persists regardless of whether focus-follows-mouse is enabled
or disabled at any given time via `komorebic`'s configuration commands.
When calling any of the `komorebic` commands related to focus-follows-mouse functionality, the `windows`
implementation will be chosen as the default implementation. You can optionally specify the `komorebi` implementation by
passing it as an argument to the `--implementation` flag:
```powershell
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
```
## Configuration with `komorebic`
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
@@ -171,51 +195,68 @@ keybindings with. You can run `komorebic.exe <COMMAND> --help` to get a full exp
each command.
```
start Start komorebi.exe as a background process
stop Stop the komorebi.exe process and restore all hidden windows
state Show a JSON representation of the current window manager state
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
focus Change focus to the window in the specified direction
move Move the focused window in the specified direction
stack Stack the focused window in the specified direction
resize Resize the focused window in the specified direction
unstack Unstack the focused window
cycle-stack Cycle the focused stack in the specified cycle direction
move-to-monitor Move the focused window to the specified monitor
move-to-workspace Move the focused window to the specified workspace
focus-monitor Focus the specified monitor
focus-workspace Focus the specified workspace on the focused monitor
new-workspace Create and append a new workspace on the focused monitor
adjust-container-padding Adjust container padding on the focused workspace
adjust-workspace-padding Adjust workspace padding on the focused workspace
change-layout Set the layout on the focused workspace
flip-layout Flip the layout on the focused workspace (BSP only)
promote Promote the focused window to the top of the tree
retile Force the retiling of all managed windows
ensure-workspaces Create at least this many workspaces for the specified monitor
container-padding Set the container padding for the specified workspace
workspace-padding Set the workspace padding for the specified workspace
workspace-layout Set the layout for the specified workspace
workspace-tiling Enable or disable window tiling for the specified workspace
workspace-name Set the workspace name for the specified workspace
toggle-pause Toggle the window manager on and off across all monitors
toggle-tiling Toggle window tiling on the focused workspace
toggle-float Toggle floating mode for the focused window
toggle-monocle Toggle monocle mode for the focused container
toggle-maximize Toggle native maximization for the focused window
restore-windows Restore all hidden windows (debugging command)
manage Force komorebi to manage the focused window
unmanage Unmanage a window that was forcibly managed
reload-configuration Reload ~/komorebi.ahk (if it exists)
watch-configuration Toggle the automatic reloading of ~/komorebi.ahk (if it exists)
float-rule Add a rule to always float the specified application
manage-rule Add a rule to always manage the specified application
workspace-rule Add a rule to associate an application with a workspace
identify-tray-application Identify an application that closes to the system tray
focus-follows-mouse Enable or disable focus follows mouse for the operating system
help Print this message or the help of the given subcommand(s)
start Start komorebi.exe as a background process
stop Stop the komorebi.exe process and restore all hidden windows
state Show a JSON representation of the current window manager state
query Query the current window manager state
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
invisible-borders Set the invisible border dimensions around each window
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
identify-border-overflow Identify an application that has overflowing borders
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
@@ -224,8 +265,10 @@ help Print this message or the help of the given subcomm
- [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
@@ -238,17 +281,22 @@ help Print this message or the help of the given subcomm
- [x] Floating rules based on exe name, window title and class
- [x] Workspace rules based on exe name and window class
- [x] Additional manage rules based on exe name and window class
- [x] Identify applications which overflow their borders by exe name and class
- [x] Identify 'close/minimize to tray' applications by exe name and class
- [x] Configure and compensate for the size of Windows 10's invisible borders
- [x] Toggle floating windows
- [x] Toggle monocle window
- [x] Toggle native maximization
- [x] Toggle focus follows mouse
- [x] Toggle Xmouse/Windows focus follows mouse implementation
- [x] Toggle Komorebi focus follows mouse implementation (desktop and system tray-aware)
- [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
- [x] Query window manager state
## Development
@@ -297,8 +345,7 @@ 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.
found, information about it will appear in the log which can be shared when opening an issue.
## Window Manager State and Integrations

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"

View File

@@ -1,5 +1,7 @@
fn main() {
windows::build!(
Windows::Win32::Devices::HumanInterfaceDevice::HID_USAGE_PAGE_GENERIC,
Windows::Win32::Devices::HumanInterfaceDevice::HID_USAGE_GENERIC_MOUSE,
Windows::Win32::Foundation::RECT,
Windows::Win32::Foundation::POINT,
Windows::Win32::Foundation::BOOL,
@@ -10,6 +12,7 @@ fn main() {
Windows::Win32::Graphics::Dwm::*,
// error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata
Windows::Win32::Graphics::Gdi::*,
Windows::Win32::System::LibraryLoader::GetModuleHandleW,
Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS,
Windows::Win32::System::Threading::PROCESS_NAME_FORMAT,
Windows::Win32::System::Threading::OpenProcess,
@@ -17,7 +20,8 @@ fn main() {
Windows::Win32::System::Threading::GetCurrentThreadId,
Windows::Win32::System::Threading::AttachThreadInput,
Windows::Win32::System::Threading::GetCurrentProcessId,
Windows::Win32::UI::KeyboardAndMouseInput::SetFocus,
// error: `Windows.Win32.UI.KeyboardAndMouseInput.RIM_TYPEMOUSE` not found in metadata
Windows::Win32::UI::KeyboardAndMouseInput::*,
Windows::Win32::UI::Accessibility::SetWinEventHook,
Windows::Win32::UI::Accessibility::HWINEVENTHOOK,
// error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata

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"

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

@@ -0,0 +1,212 @@
#![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::option::Option::Some;
use ::std::string::ToString;
use ::std::unreachable;
use ::quote::quote;
use ::syn::parse_macro_input;
use ::syn::Data;
use ::syn::DataEnum;
use ::syn::DeriveInput;
use ::syn::Fields;
use ::syn::FieldsNamed;
use ::syn::FieldsUnnamed;
use ::syn::Meta;
use ::syn::NestedMeta;
#[allow(clippy::too_many_lines)]
#[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 argument_idents = named
.iter()
// Filter out the flags
.filter(|&f| {
let mut include = true;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
if path.is_ident("long") {
include = false;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let argument_idents_clone = argument_idents.clone();
let called_arguments = quote! {#(%#argument_idents_clone%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
let flag_idents = named
.iter()
// Filter only the flags
.filter(|f| {
let mut include = false;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
// Identify them using the --long flag name
if path.is_ident("long") {
include = true;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let has_flags = flag_idents.clone().count() != 0;
if has_flags {
let flag_idents_concat = flag_idents.clone();
let argument_idents_concat = argument_idents.clone();
// Concat the args and flag args if there are flags
let all_arguments =
quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *}
.to_string();
let flag_idents_clone = flag_idents.clone();
let flags = quote! {#(--#flag_idents_clone) *}
.to_string()
.replace("- - ", "--");
let called_flag_arguments = quote! {#(%#flag_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),
#all_arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments,
#flags,
#called_flag_arguments
)
}
}
}
} else {
let arguments = quote! {#(#argument_idents), *}.to_string();
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
Run, komorebic.exe {} {}, , Hide
}}"#,
::std::stringify!(#name),
#arguments,
::std::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.1"
version = "0.1.3"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -32,6 +32,8 @@ pub enum SocketMessage {
CycleStack(CycleDirection),
MoveContainerToMonitorNumber(usize),
MoveContainerToWorkspaceNumber(usize),
SendContainerToMonitorNumber(usize),
SendContainerToWorkspaceNumber(usize),
Promote,
ToggleFloat,
ToggleMonocle,
@@ -60,12 +62,16 @@ pub enum SocketMessage {
// Configuration
ReloadConfiguration,
WatchConfiguration(bool),
InvisibleBorders(Rect),
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
FloatRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
IdentifyTrayApplication(ApplicationIdentifier, String),
IdentifyBorderOverflow(ApplicationIdentifier, String),
State,
FocusFollowsMouse(bool),
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
}
impl SocketMessage {
@@ -86,6 +92,15 @@ impl FromStr for SocketMessage {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum StateQuery {
FocusedMonitorIndex,
FocusedWorkspaceIndex,
FocusedContainerIndex,
FocusedWindowIndex,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum ApplicationIdentifier {
@@ -94,6 +109,13 @@ pub enum ApplicationIdentifier {
Title,
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum FocusFollowsMouseImplementation {
Komorebi,
Windows,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
#[strum(serialize_all = "snake_case")]
pub enum Sizing {

View File

@@ -1,8 +1,9 @@
use serde::Deserialize;
use serde::Serialize;
use bindings::Windows::Win32::Foundation::RECT;
#[derive(Debug, Clone, Copy, Serialize, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
pub struct Rect {
pub left: i32,
pub top: i32,
@@ -10,17 +11,6 @@ pub struct Rect {
pub bottom: i32,
}
impl Default for Rect {
fn default() -> Self {
Self {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
}
}
impl From<RECT> for Rect {
fn from(rect: RECT) -> Self {
Self {

View File

@@ -3,6 +3,9 @@
; Enable hot reloading of changes to this file
Run, komorebic.exe watch-configuration enable, , Hide
; Configure the invisible border dimensions
Run, komorebic.exe invisible-borders 7 0 14 7, , Hide
; Enable focus follows mouse
Run, komorebic.exe focus-follows-mouse enable, , Hide
@@ -52,6 +55,9 @@ 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
; Identify applications that have overflowing borders
Run, komorebic.exe identify-border-overflow exe Discord.exe, , Hide
; Change the focused window, Alt + Vim direction keys
!h::
Run, komorebic.exe focus left, , Hide

View File

@@ -0,0 +1,232 @@
#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 invisible border dimensions
InvisibleBorders(7, 0, 14, 7)
; 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")
IdentifyTrayApplication("exe", "Spotify.exe")
; Identify Electron applications with overflowing borders
IdentifyBorderOverflow("exe", "Discord.exe")
IdentifyBorderOverflow("exe", "Spotify.exe")
IdentifyBorderOverflow("class", "ZPFTEWndClass")
; 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
; Enable focus follows mouse
!0::
ToggleFocusFollowsMouse("komorebi")
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,11 @@
[package]
name = "komorebi"
version = "0.1.1"
version = "0.1.3"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
license = "MIT"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -10,12 +15,12 @@ bindings = { package = "bindings", path = "../bindings" }
komorebi-core = { path = "../komorebi-core" }
bitflags = "1"
clap = "3.0.0-beta.4"
color-eyre = "0.5"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
ctrlc = "3"
dirs = "3"
eyre = "0.6"
getset = "0.1"
hotwatch = "0.4"
lazy_static = "1"
@@ -31,6 +36,7 @@ tracing-appender = "0.1"
tracing-subscriber = "0.2"
uds_windows = "1"
which = "4"
winput = "0.2"
winvd = "0.0.20"
[features]

View File

@@ -44,6 +44,18 @@ impl Container {
}
}
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
for window in self.windows() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(window.hwnd);
}
}
}
None
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for window in self.windows() {
if window.hwnd == hwnd {

View File

@@ -3,13 +3,16 @@
use std::collections::HashMap;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
#[cfg(feature = "deadlock_detection")]
use std::thread;
#[cfg(feature = "deadlock_detection")]
use std::time::Duration;
use color_eyre::eyre::ContextCompat;
use clap::Clap;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
@@ -25,6 +28,7 @@ use which::which;
use crate::process_command::listen_for_commands;
use crate::process_event::listen_for_events;
use crate::process_movement::listen_for_movements;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
@@ -36,6 +40,7 @@ mod container;
mod monitor;
mod process_command;
mod process_event;
mod process_movement;
mod set_window_position;
mod styles;
mod window;
@@ -51,16 +56,15 @@ lazy_static! {
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()]));
static ref TRAY_AND_MULTI_WINDOW_CLASSES: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![]));
static ref TRAY_AND_MULTI_WINDOW_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"explorer.exe".to_string(),
"firefox.exe".to_string(),
"chrome.exe".to_string(),
"idea64.exe".to_string(),
"ApplicationFrameHost.exe".to_string(),
"steam.exe".to_string(),
]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
Arc::new(Mutex::new(vec![
"explorer.exe".to_string(),
"firefox.exe".to_string(),
"chrome.exe".to_string(),
"idea64.exe".to_string(),
"ApplicationFrameHost.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(),
@@ -69,8 +73,11 @@ lazy_static! {
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![]));
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
}
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
@@ -82,7 +89,7 @@ fn setup() -> Result<(WorkerGuard, 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);
@@ -133,7 +140,7 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
}
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");
@@ -147,7 +154,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")
@@ -159,7 +166,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")
@@ -193,61 +200,76 @@ fn detect_deadlocks() {
});
}
#[derive(Clap)]
#[clap(author, about, version)]
struct Opts {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(long = "ffm")]
focus_follows_mouse: bool,
}
#[tracing::instrument]
fn main() -> Result<()> {
match std::env::args().count() {
1 => {
let mut system = sysinfo::System::new_all();
system.refresh_processes();
let opts: Opts = Opts::parse();
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
if system.process_by_name("komorebi.exe").len() > 1 {
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
std::process::exit(1);
}
let arg_count = std::env::args().count();
let has_valid_args = arg_count == 1 || (arg_count == 2 && CUSTOM_FFM.load(Ordering::SeqCst));
// File logging worker guard has to have an assignment in the main fn to work
let (_guard, _color_guard) = setup()?;
if has_valid_args {
let mut system = sysinfo::System::new_all();
system.refresh_processes();
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
let process_id = WindowsApi::current_process_id();
WindowsApi::allow_set_foreground_window(process_id)?;
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
incoming,
)))?));
wm.lock().init()?;
listen_for_commands(wm.clone());
listen_for_events(wm.clone());
load_configuration()?;
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
ctrlc_sender
.send(())
.expect("could not send signal on ctrl-c channel");
})?;
ctrlc_receiver
.recv()
.expect("could not receive signal on ctrl-c channel");
tracing::error!(
"received ctrl-c, restoring all hidden windows and terminating process"
);
wm.lock().restore_all_windows();
std::process::exit(130);
if system.process_by_name("komorebi.exe").len() > 1 {
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
std::process::exit(1);
}
_ => Ok(()),
// File logging worker guard has to have an assignment in the main fn to work
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)?;
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
crossbeam_channel::unbounded();
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
winevent_listener.start();
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
incoming,
)))?));
wm.lock().init()?;
listen_for_commands(wm.clone());
listen_for_events(wm.clone());
if CUSTOM_FFM.load(Ordering::SeqCst) {
listen_for_movements(wm.clone());
}
load_configuration()?;
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
ctrlc_sender
.send(())
.expect("could not send signal on ctrl-c channel");
})?;
ctrlc_receiver
.recv()
.expect("could not receive signal on ctrl-c channel");
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
wm.lock().restore_all_windows();
std::process::exit(130);
}
Ok(())
}

View File

@@ -1,11 +1,12 @@
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;
use getset::MutGetters;
use getset::Setters;
use serde::Serialize;
use komorebi_core::Rect;
@@ -14,9 +15,9 @@ use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters)]
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
pub struct Monitor {
#[getset(get_copy = "pub")]
#[getset(get_copy = "pub", set = "pub")]
id: isize,
monitor_size: Rect,
#[getset(get = "pub")]
@@ -30,11 +31,14 @@ pub struct Monitor {
impl_ring_elements!(Monitor, Workspace);
pub fn new(id: isize, monitor_size: Rect, work_area_size: Rect) -> Monitor {
let mut workspaces = Ring::default();
workspaces.elements_mut().push_back(Workspace::default());
Monitor {
id,
monitor_size,
work_area_size,
workspaces: Ring::default(),
workspaces,
workspace_names: HashMap::default(),
}
}
@@ -56,7 +60,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);
@@ -78,17 +82,17 @@ impl Monitor {
) -> Result<()> {
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(eyre::anyhow!(
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();
@@ -129,7 +133,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);
}
}
@@ -141,12 +145,12 @@ impl Monitor {
self.workspaces().len()
}
pub fn update_focused_workspace(&mut self) -> Result<()> {
pub fn update_focused_workspace(&mut self, invisible_borders: &Rect) -> Result<()> {
let work_area = *self.work_area_size();
self.focused_workspace_mut()
.context("there is no workspace")?
.update(&work_area)?;
.ok_or_else(|| anyhow!("there is no workspace"))?
.update(&work_area, invisible_borders)?;
Ok(())
}

View File

@@ -2,24 +2,27 @@ use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::str::FromStr;
use std::sync::atomic::Ordering;
use std::sync::Arc;
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;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;
use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::CUSTOM_FFM;
use crate::FLOAT_IDENTIFIERS;
use crate::MANAGE_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
use crate::TRAY_AND_MULTI_WINDOW_EXES;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
#[tracing::instrument]
@@ -106,8 +109,19 @@ 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");
if self.is_paused {
tracing::info!("resuming");
} else {
tracing::info!("pausing");
}
self.is_paused = !self.is_paused;
}
SocketMessage::ToggleTiling => {
@@ -117,21 +131,7 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(true)?;
}
SocketMessage::Retile => {
for monitor in self.monitors_mut() {
let work_area = *monitor.work_area_size();
let workspace = monitor
.focused_workspace_mut()
.context("there is no workspace")?;
// Reset any resize adjustments if we want to force a retile
for resize in workspace.resize_dimensions_mut() {
*resize = None;
}
workspace.update(&work_area)?;
}
}
SocketMessage::Retile => self.retile_all()?,
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?,
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
@@ -141,6 +141,15 @@ impl WindowManager {
self.set_workspace_layout(monitor_idx, workspace_idx, layout)?;
}
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
let monitor_idx = self.monitor_idx_from_current_pos().ok_or_else(|| {
anyhow!("there is no monitor associated with the current cursor position")
})?;
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
SocketMessage::Stop => {
@@ -161,21 +170,132 @@ 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();
let mut stream = UnixStream::connect(&socket)?;
stream.write_all(state.as_bytes())?;
}
SocketMessage::Query(query) => {
let response = match query {
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
StateQuery::FocusedWorkspaceIndex => self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx(),
StateQuery::FocusedContainerIndex => {
self.focused_workspace()?.focused_container_idx()
}
StateQuery::FocusedWindowIndex => {
self.focused_container()?.focused_window_idx()
}
}
.to_string();
let mut socket =
dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
socket.push("komorebic.sock");
let socket = socket.as_path();
let mut stream = UnixStream::connect(&socket)?;
stream.write_all(response.as_bytes())?;
}
SocketMessage::ResizeWindow(direction, sizing) => {
self.resize_window(direction, sizing, Option::from(50))?;
}
SocketMessage::FocusFollowsMouse(enable) => {
if enable {
WindowsApi::enable_focus_follows_mouse()?;
} else {
WindowsApi::disable_focus_follows_mouse()?;
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
if !CUSTOM_FFM.load(Ordering::SeqCst) {
tracing::warn!(
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be enabled; defaulting to windows implementation"
);
implementation = FocusFollowsMouseImplementation::Windows;
}
match implementation {
FocusFollowsMouseImplementation::Komorebi => {
if WindowsApi::focus_follows_mouse()? {
tracing::warn!(
"the komorebi implementation of focus follows mouse cannot be enabled while the windows implementation is enabled"
);
} else if enable {
self.focus_follows_mouse = Option::from(implementation);
} else {
self.focus_follows_mouse = None;
self.has_pending_raise_op = false;
}
}
FocusFollowsMouseImplementation::Windows => {
if let Some(FocusFollowsMouseImplementation::Komorebi) =
self.focus_follows_mouse
{
tracing::warn!(
"the windows implementation of focus follows mouse cannot be enabled while the komorebi implementation is enabled"
);
} else if enable {
WindowsApi::enable_focus_follows_mouse()?;
self.focus_follows_mouse =
Option::from(FocusFollowsMouseImplementation::Windows);
} else {
WindowsApi::disable_focus_follows_mouse()?;
self.focus_follows_mouse = None;
}
}
}
}
SocketMessage::ToggleFocusFollowsMouse(mut implementation) => {
if !CUSTOM_FFM.load(Ordering::SeqCst) {
tracing::warn!(
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be toggled; defaulting to windows implementation"
);
implementation = FocusFollowsMouseImplementation::Windows;
}
match implementation {
FocusFollowsMouseImplementation::Komorebi => {
if WindowsApi::focus_follows_mouse()? {
tracing::warn!(
"the komorebi implementation of focus follows mouse cannot be toggled while the windows implementation is enabled"
);
} else {
match self.focus_follows_mouse {
None => {
self.focus_follows_mouse = Option::from(implementation);
self.has_pending_raise_op = false;
}
Some(FocusFollowsMouseImplementation::Komorebi) => {
self.focus_follows_mouse = None;
}
Some(FocusFollowsMouseImplementation::Windows) => {
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
}
}
}
}
FocusFollowsMouseImplementation::Windows => {
if let Some(FocusFollowsMouseImplementation::Komorebi) =
self.focus_follows_mouse
{
tracing::warn!(
"the windows implementation of focus follows mouse cannot be toggled while the komorebi implementation is enabled"
);
} else {
match self.focus_follows_mouse {
None => {
WindowsApi::enable_focus_follows_mouse()?;
self.focus_follows_mouse = Option::from(implementation);
}
Some(FocusFollowsMouseImplementation::Windows) => {
WindowsApi::disable_focus_follows_mouse()?;
self.focus_follows_mouse = None;
}
Some(FocusFollowsMouseImplementation::Komorebi) => {
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
}
}
}
}
}
}
SocketMessage::ReloadConfiguration => {
@@ -184,28 +304,29 @@ impl WindowManager {
SocketMessage::WatchConfiguration(enable) => {
self.watch_configuration(enable)?;
}
SocketMessage::IdentifyTrayApplication(identifier, id) => match identifier {
ApplicationIdentifier::Exe => {
let mut exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
if !exes.contains(&id) {
exes.push(id);
}
SocketMessage::IdentifyBorderOverflow(_, id) => {
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
ApplicationIdentifier::Class => {
let mut classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
if !classes.contains(&id) {
classes.push(id);
}
}
SocketMessage::IdentifyTrayApplication(_, id) => {
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
if !identifiers.contains(&id) {
identifiers.push(id);
}
ApplicationIdentifier::Title => {}
},
}
SocketMessage::ManageFocusedWindow => {
self.manage_focused_window()?;
}
SocketMessage::UnmanageFocusedWindow => {
self.unmanage_focused_window()?;
}
}
SocketMessage::InvisibleBorders(rect) => {
self.invisible_borders = rect;
self.retile_all()?;
}
};
tracing::info!("processed");
Ok(())
@@ -218,14 +339,15 @@ impl WindowManager {
let message = SocketMessage::from_str(&line?)?;
if self.is_paused {
if let SocketMessage::TogglePause = message {
tracing::info!("resuming");
self.is_paused = !self.is_paused;
return Ok(());
}
tracing::trace!("ignoring while paused");
return Ok(());
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(self.process_command(message)?)
}
_ => {
tracing::trace!("ignoring while paused");
Ok(())
}
};
}
self.process_command(message)?;

View File

@@ -2,7 +2,7 @@ use std::fs::OpenOptions;
use std::sync::Arc;
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;
@@ -15,8 +15,7 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::HIDDEN_HWNDS;
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
use crate::TRAY_AND_MULTI_WINDOW_EXES;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
@@ -55,21 +54,24 @@ impl WindowManager {
WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Show(_, window)
| 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")?;
self.reconcile_monitors()?;
let monitor_idx = self.monitor_idx_from_window(*window)
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
self.focus_monitor(monitor_idx)?;
}
_ => {}
}
let invisible_borders = self.invisible_borders;
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area)?;
workspace.update(&work_area, &invisible_borders)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
@@ -91,6 +93,10 @@ impl WindowManager {
}
match event {
WindowManagerEvent::Raise(window) => {
window.raise()?;
self.has_pending_raise_op = false;
}
WindowManagerEvent::Minimize(_, window)
| WindowManagerEvent::Destroy(_, window)
| WindowManagerEvent::Unmanage(window) => {
@@ -105,15 +111,16 @@ impl WindowManager {
// 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();
let tray_and_multi_window_classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
let tray_and_multi_window_identifiers =
TRAY_AND_MULTI_WINDOW_IDENTIFIERS.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()?)
if (!window.is_window()
|| tray_and_multi_window_identifiers.contains(&window.exe()?))
|| tray_and_multi_window_identifiers.contains(&window.class()?)
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
{
hide = true;
@@ -154,16 +161,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(());
}
}
@@ -210,22 +217,14 @@ 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
let border = Rect {
left: 12,
top: 0,
right: 24,
bottom: 12,
};
// Adjust for the invisible border
new_position.left += border.left;
new_position.top += border.top;
new_position.right -= border.right;
new_position.bottom -= border.bottom;
// Adjust for the invisible borders
new_position.left += invisible_borders.left;
new_position.top += invisible_borders.top;
new_position.right -= invisible_borders.right;
new_position.bottom -= invisible_borders.bottom;
let resize = Rect {
left: new_position.left - old_position.left,
@@ -290,7 +289,7 @@ impl WindowManager {
// 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()?)?;
window.center(&self.focused_monitor_work_area()?, &invisible_borders)?;
}
tracing::trace!("updating list of known hwnds");
@@ -305,7 +304,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)
@@ -314,6 +314,7 @@ impl WindowManager {
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &known_hwnds)?;
tracing::info!("processed: {}", event.window().to_string());
Ok(())
}

View File

@@ -0,0 +1,41 @@
use std::sync::Arc;
use parking_lot::Mutex;
use winput::message_loop;
use winput::message_loop::Event;
use winput::Action;
use komorebi_core::FocusFollowsMouseImplementation;
use crate::window_manager::WindowManager;
#[tracing::instrument]
pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
let mut ignore_movement = false;
let receiver = message_loop::start().expect("could not start winput message loop");
loop {
let focus_follows_mouse = wm.lock().focus_follows_mouse.clone();
if let Some(FocusFollowsMouseImplementation::Komorebi) = focus_follows_mouse {
match receiver.next_event() {
// Don't want to send any raise events while we are dragging or resizing
Event::MouseButton { action, .. } => match action {
Action::Press => ignore_movement = true,
Action::Release => ignore_movement = false,
},
Event::MouseMoveRelative { .. } => {
if !ignore_movement {
match wm.lock().raise_window_at_cursor_pos() {
Ok(_) => {}
Err(error) => tracing::error!("{}", error),
}
}
}
_ => {}
}
}
}
});
}

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,6 +15,7 @@ use crate::styles::GwlExStyle;
use crate::styles::GwlStyle;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDDEN_HWNDS;
use crate::LAYERED_EXE_WHITELIST;
@@ -70,7 +71,7 @@ impl Window {
HWND(self.hwnd)
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
pub fn center(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
@@ -81,44 +82,35 @@ impl Window {
right: half_width,
bottom: half_weight,
},
invisible_borders,
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
// hard coding the border Rect to avoid two calls to set_window_pos and making the screen
// flicker on container/window movement. Still not 100% sure if this is DPI-aware.
// Set the new position first to be able to get the extended frame bounds
// WindowsApi::set_window_pos(self.hwnd(), layout, false, false)?;
// let mut rect = WindowsApi::window_rect(self.hwnd())?;
// Get the extended frame bounds of the new position
// let frame = WindowsApi::window_rect_with_extended_frame_bounds(self.hwnd())?;
// Calculate the invisible border diff
// let border = Rect {
// left: frame.left - rect.left,
// top: frame.top - rect.top,
// right: rect.right - frame.right,
// bottom: rect.bottom - frame.bottom,
// };
pub fn set_position(
&mut self,
layout: &Rect,
invisible_borders: &Rect,
top: bool,
) -> Result<()> {
let mut rect = *layout;
let border = Rect {
left: 12,
top: 0,
right: 24,
bottom: 12,
};
let mut should_remove_border = true;
// Remove the invisible border
rect.left -= border.left;
rect.top -= border.top;
rect.right += border.right;
rect.bottom += border.bottom;
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
if border_overflows.contains(&self.title()?)
|| border_overflows.contains(&self.exe()?)
|| border_overflows.contains(&self.class()?)
{
should_remove_border = false;
}
if should_remove_border {
// Remove the invisible borders
rect.left -= invisible_borders.left;
rect.top -= invisible_borders.top;
rect.right += invisible_borders.right;
rect.bottom += invisible_borders.bottom;
}
WindowsApi::position_window(self.hwnd(), &rect, top)
}
@@ -156,6 +148,27 @@ impl Window {
WindowsApi::maximize_window(self.hwnd());
}
pub fn raise(self) -> Result<()> {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
let current_thread_id = WindowsApi::current_thread_id();
WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true)?;
// Raise Window to foreground
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(_) => {}
Err(error) => {
tracing::error!(
"could not set as foreground window, but continuing execution of focus(): {}",
error
);
}
};
// This isn't really needed when the above command works as expected via AHK
WindowsApi::set_focus(self.hwnd())
}
pub fn focus(self) -> Result<()> {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
@@ -187,12 +200,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> {

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use crossbeam_channel::Receiver;
@@ -16,6 +17,7 @@ use uds_windows::UnixListener;
use komorebi_core::CycleDirection;
use komorebi_core::Flip;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
@@ -30,11 +32,11 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
use crate::workspace::Workspace;
use crate::BORDER_OVERFLOW_IDENTIFIERS;
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::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
#[derive(Debug)]
@@ -43,19 +45,25 @@ pub struct WindowManager {
pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>,
pub command_listener: UnixListener,
pub is_paused: bool,
pub invisible_borders: Rect,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub hotwatch: Hotwatch,
pub virtual_desktop_id: Option<usize>,
pub has_pending_raise_op: bool,
}
#[derive(Debug, Serialize)]
pub struct State {
pub monitors: Ring<Monitor>,
pub is_paused: bool,
pub invisible_borders: Rect,
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
pub has_pending_raise_op: bool,
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>,
pub tray_and_multi_window_identifiers: Vec<String>,
pub border_overflow_identifiers: Vec<String>,
}
#[allow(clippy::fallible_impl_from)]
@@ -64,11 +72,14 @@ impl From<&mut WindowManager> for State {
Self {
monitors: wm.monitors.clone(),
is_paused: wm.is_paused,
invisible_borders: wm.invisible_borders,
focus_follows_mouse: wm.focus_follows_mouse.clone(),
has_pending_raise_op: wm.has_pending_raise_op,
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(),
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
border_overflow_identifiers: BORDER_OVERFLOW_IDENTIFIERS.lock().clone(),
}
}
}
@@ -102,7 +113,7 @@ impl EnforceWorkspaceRuleOp {
impl WindowManager {
#[tracing::instrument]
pub fn new(incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>) -> Result<Self> {
let home = dirs::home_dir().context("there is no home directory")?;
let 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();
@@ -127,8 +138,16 @@ impl WindowManager {
incoming_events: incoming,
command_listener: listener,
is_paused: false,
invisible_borders: Rect {
left: 7,
top: 0,
right: 14,
bottom: 7,
},
focus_follows_mouse: None,
hotwatch: Hotwatch::new()?,
virtual_desktop_id,
has_pending_raise_op: false,
})
}
@@ -148,7 +167,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");
@@ -173,7 +192,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
@@ -204,7 +223,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)?;
@@ -214,6 +233,44 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn reconcile_monitors(&mut self) -> Result<()> {
let valid_hmonitors = WindowsApi::valid_hmonitors()?;
let mut invalid = vec![];
for monitor in self.monitors_mut() {
if !valid_hmonitors.contains(&monitor.id()) {
let mut mark_as_invalid = true;
// If an invalid hmonitor has at least one window in the window manager state,
// we can attempt to update its hmonitor id in-place so that it doesn't get reaped
if let Some(workspace) = monitor.focused_workspace() {
if let Some(container) = workspace.focused_container() {
if let Some(window) = container.focused_window() {
let actual_hmonitor = WindowsApi::monitor_from_window(window.hwnd());
if actual_hmonitor != monitor.id() {
monitor.set_id(actual_hmonitor);
mark_as_invalid = false;
}
}
}
}
if mark_as_invalid {
invalid.push(monitor.id());
}
}
}
// Remove any invalid monitors from our state
self.monitors_mut().retain(|m| !invalid.contains(&m.id()));
// Check for and add any new monitors that may have been plugged in
WindowsApi::load_monitor_information(&mut self.monitors)?;
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn enforce_workspace_rules(&mut self) -> Result<()> {
let mut to_move = vec![];
@@ -222,7 +279,7 @@ impl WindowManager {
let focused_workspace_idx = self
.monitors()
.get(focused_monitor_idx)
.context("there is no monitor with that index")?
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.focused_workspace_idx();
let workspace_rules = WORKSPACE_RULES.lock();
@@ -283,10 +340,10 @@ impl WindowManager {
let origin_workspace = self
.monitors_mut()
.get_mut(op.origin_monitor_idx)
.context("there is no monitor with that index")?
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.workspaces_mut()
.get_mut(op.origin_workspace_idx)
.context("there is no workspace with that index")?;
.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) {
@@ -303,7 +360,7 @@ impl WindowManager {
let target_monitor = self
.monitors_mut()
.get_mut(op.target_monitor_idx)
.context("there is no monitor with that index")?;
.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
@@ -318,7 +375,7 @@ impl WindowManager {
let target_workspace = target_monitor
.workspaces_mut()
.get_mut(op.target_workspace_idx)
.context("there is no workspace with that index")?;
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
target_workspace.new_container_for_window(Window { hwnd: op.hwnd });
}
@@ -331,6 +388,26 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn retile_all(&mut self) -> Result<()> {
let invisible_borders = self.invisible_borders;
for monitor in self.monitors_mut() {
let work_area = *monitor.work_area_size();
let workspace = monitor
.focused_workspace_mut()
.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() {
*resize = None;
}
workspace.update(&work_area, &invisible_borders)?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn validate_virtual_desktop_id(&self) {
let virtual_desktop_id = winvd::helpers::get_current_desktop_number().ok();
@@ -359,13 +436,76 @@ impl WindowManager {
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
}
#[tracing::instrument(skip(self))]
pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> {
let mut hwnd = WindowsApi::window_at_cursor_pos()?;
if self.has_pending_raise_op
|| self.focused_window()?.hwnd == hwnd
// Sometimes we need this check, because the focus may have been given by a click
// to a non-window such as the taskbar or system tray, and komorebi doesn't know that
// the focused window of the workspace is not actually focused by the OS at that point
|| WindowsApi::foreground_window()? == hwnd
{
Ok(())
} else {
let mut known_hwnd = false;
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
if workspace.contains_window(hwnd) {
known_hwnd = true;
}
}
}
// TODO: Not sure if this needs to be made configurable just yet...
let overlay_classes = [
// Chromium/Electron
"Chrome_RenderWidgetHostHWND".to_string(),
// Explorer
"DirectUIHWND".to_string(),
"SysTreeView32".to_string(),
"ToolbarWindow32".to_string(),
"NetUIHWND".to_string(),
];
if !known_hwnd {
let class = Window { hwnd }.class()?;
// Some applications (Electron/Chromium-based, explorer) have (invisible?) overlays
// windows that we need to look beyond to find the actual window to raise
if overlay_classes.contains(&class) {
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
if let Some(exe_hwnd) = workspace.hwnd_from_exe(&Window { hwnd }.exe()?)
{
hwnd = exe_hwnd;
known_hwnd = true;
}
}
}
}
}
if known_hwnd {
let event = WindowManagerEvent::Raise(Window { hwnd });
self.has_pending_raise_op = true;
Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?)
} else {
tracing::debug!("not raising unknown window: {}", Window { hwnd });
Ok(())
}
}
}
#[tracing::instrument(skip(self))]
pub fn update_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
tracing::info!("updating");
let invisible_borders = self.invisible_borders;
self.focused_monitor_mut()
.context("there is no monitor")?
.update_focused_workspace()?;
.ok_or_else(|| anyhow!("there is no monitor"))?
.update_focused_workspace(&invisible_borders)?;
if mouse_follows_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
@@ -385,7 +525,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!()))?;
}
}
@@ -408,7 +548,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(),
@@ -453,7 +593,7 @@ impl WindowManager {
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,
@@ -487,28 +627,33 @@ 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 invisible_borders = self.invisible_borders;
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(eyre::anyhow!(
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()?;
target_monitor.update_focused_workspace(&invisible_borders)?;
if follow {
self.focus_monitor(idx)?;
@@ -521,9 +666,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)
}
@@ -534,7 +683,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()?;
@@ -551,7 +700,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);
@@ -565,7 +714,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();
@@ -592,9 +741,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
@@ -623,7 +772,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()?;
@@ -666,6 +815,7 @@ impl WindowManager {
tracing::info!("floating window");
let work_area = self.focused_monitor_work_area()?;
let invisible_borders = self.invisible_borders;
let workspace = self.focused_workspace_mut()?;
workspace.new_floating_window()?;
@@ -673,9 +823,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"))?;
window.center(&work_area)?;
window.center(&work_area, &invisible_borders)?;
window.focus()?;
Ok(())
@@ -805,7 +955,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)));
@@ -820,7 +970,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)));
@@ -837,12 +987,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);
@@ -858,12 +1008,13 @@ impl WindowManager {
) -> Result<()> {
tracing::info!("setting workspace layout");
let invisible_borders = self.invisible_borders;
let focused_monitor_idx = self.focused_monitor_idx();
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();
@@ -871,13 +1022,13 @@ 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);
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area)?;
workspace.update(&work_area, &invisible_borders)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
@@ -895,7 +1046,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);
@@ -914,12 +1065,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));
@@ -938,12 +1089,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);
@@ -963,12 +1114,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));
@@ -978,7 +1129,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())
}
@@ -989,7 +1140,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(())
@@ -1007,18 +1158,30 @@ impl WindowManager {
None
}
pub fn monitor_idx_from_current_pos(&mut self) -> Option<usize> {
let hmonitor = WindowsApi::monitor_from_point(WindowsApi::cursor_pos().ok()?);
for (i, monitor) in self.monitors().iter().enumerate() {
if monitor.id() == hmonitor {
return Option::from(i);
}
}
None
}
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))]
@@ -1027,7 +1190,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()?;
@@ -1041,7 +1204,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()?;
@@ -1052,18 +1215,24 @@ 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"))
}
pub fn focused_window(&self) -> Result<&Window> {
self.focused_container()?
.focused_window()
.ok_or_else(|| anyhow!("there is no window"))
}
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

@@ -16,6 +16,7 @@ pub enum WindowManagerEvent {
MouseCapture(WinEvent, Window),
Manage(Window),
Unmanage(Window),
Raise(Window),
}
impl Display for WindowManagerEvent {
@@ -60,6 +61,9 @@ impl Display for WindowManagerEvent {
winevent, window
)
}
WindowManagerEvent::Raise(window) => {
write!(f, "Raise (Window: {})", window)
}
}
}
}
@@ -74,6 +78,7 @@ impl WindowManagerEvent {
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::MoveResizeEnd(_, window)
| WindowManagerEvent::MouseCapture(_, window)
| WindowManagerEvent::Raise(window)
| WindowManagerEvent::Manage(window)
| WindowManagerEvent::Unmanage(window) => window,
}

View File

@@ -3,12 +3,15 @@ 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::Devices::HumanInterfaceDevice::HID_USAGE_GENERIC_MOUSE;
use bindings::Windows::Win32::Devices::HumanInterfaceDevice::HID_USAGE_PAGE_GENERIC;
use bindings::Windows::Win32::Foundation::BOOL;
use bindings::Windows::Win32::Foundation::HANDLE;
use bindings::Windows::Win32::Foundation::HINSTANCE;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::Foundation::LPARAM;
use bindings::Windows::Win32::Foundation::POINT;
@@ -23,12 +26,15 @@ use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
use bindings::Windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use bindings::Windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use bindings::Windows::Win32::Graphics::Gdi::MonitorFromPoint;
use bindings::Windows::Win32::Graphics::Gdi::MonitorFromWindow;
use bindings::Windows::Win32::Graphics::Gdi::HBRUSH;
use bindings::Windows::Win32::Graphics::Gdi::HDC;
use bindings::Windows::Win32::Graphics::Gdi::HMONITOR;
use bindings::Windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use bindings::Windows::Win32::Graphics::Gdi::MONITORINFO;
use bindings::Windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use bindings::Windows::Win32::System::LibraryLoader::GetModuleHandleW;
use bindings::Windows::Win32::System::Threading::AttachThreadInput;
use bindings::Windows::Win32::System::Threading::GetCurrentProcessId;
use bindings::Windows::Win32::System::Threading::GetCurrentThreadId;
@@ -37,9 +43,22 @@ use bindings::Windows::Win32::System::Threading::QueryFullProcessImageNameW;
use bindings::Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
use bindings::Windows::Win32::System::Threading::PROCESS_NAME_FORMAT;
use bindings::Windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::GetRawInputBuffer;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::GetRawInputData;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::RegisterRawInputDevices;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::SetFocus;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::HRAWINPUT;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::RAWINPUT;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::RAWINPUTDEVICE;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::RAWINPUTHEADER;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::RIDEV_INPUTSINK;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::RIDEV_NOLEGACY;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::RID_INPUT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::DestroyWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use bindings::Windows::Win32::UI::WindowsAndMessaging::FindWindowExW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
@@ -53,28 +72,42 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::IsIconic;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
use bindings::Windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::RegisterClassExW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use bindings::Windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::UnregisterClassW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
use bindings::Windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HCURSOR;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HICON;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HMENU;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_MESSAGE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
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;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WINDOW_EX_STYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WINDOW_STYLE;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDCLASSEXW;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDCLASS_STYLES;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDPROC;
use komorebi_core::Rect;
use crate::container::Container;
@@ -83,7 +116,21 @@ use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::workspace::Workspace;
pub trait IntoPWSTR {
fn into_pwstr(self) -> PWSTR;
}
impl IntoPWSTR for &str {
fn into_pwstr(self) -> PWSTR {
PWSTR(
self.encode_utf16()
.chain([0_u16])
.collect::<Vec<u16>>()
.as_mut_ptr(),
)
}
}
pub enum WindowsResult<T, E> {
Err(E),
@@ -100,6 +147,16 @@ impl From<BOOL> for WindowsResult<(), Error> {
}
}
impl From<HINSTANCE> for WindowsResult<HINSTANCE, Error> {
fn from(return_value: HINSTANCE) -> Self {
if return_value.is_null() {
Self::Err(std::io::Error::last_os_error().into())
} else {
Self::Ok(return_value)
}
}
}
impl From<HWND> for WindowsResult<isize, Error> {
fn from(return_value: HWND) -> Self {
if return_value.is_null() {
@@ -135,7 +192,7 @@ macro_rules! impl_from_integer_for_windows_result {
};
}
impl_from_integer_for_windows_result!(isize, u32, i32);
impl_from_integer_for_windows_result!(isize, u16, u32, i32);
impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
fn from(result: WindowsResult<T, E>) -> Self {
@@ -163,6 +220,57 @@ impl WindowsApi {
}))
}
#[allow(dead_code)]
pub fn valid_hwnds() -> Result<Vec<isize>> {
let mut hwnds: Vec<isize> = vec![];
let hwnds_ref: &mut Vec<isize> = hwnds.as_mut();
Self::enum_windows(
windows_callbacks::valid_hwnds,
hwnds_ref as *mut Vec<isize> as isize,
)?;
Ok(hwnds)
}
#[allow(dead_code)]
pub fn hwnd_by_class(class: &str) -> Option<isize> {
let hwnds = Self::valid_hwnds().ok()?;
for hwnd in hwnds {
if let Ok(hwnd_class) = Self::real_window_class_w(HWND(hwnd)) {
if hwnd_class == class {
return Option::from(hwnd);
}
}
}
None
}
#[allow(dead_code)]
pub fn hwnd_by_title(class: &str) -> Option<isize> {
let hwnds = Self::valid_hwnds().ok()?;
for hwnd in hwnds {
if let Ok(hwnd_title) = Self::window_text_w(HWND(hwnd)) {
if hwnd_title == class {
return Option::from(hwnd);
}
}
}
None
}
pub fn valid_hmonitors() -> Result<Vec<isize>> {
let mut monitors: Vec<isize> = vec![];
let monitors_ref: &mut Vec<isize> = monitors.as_mut();
Self::enum_display_monitors(
windows_callbacks::valid_display_monitors,
monitors_ref as *mut Vec<isize> as isize,
)?;
Ok(monitors)
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
Self::enum_display_monitors(
windows_callbacks::enum_display_monitor,
@@ -178,9 +286,8 @@ impl WindowsApi {
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
for monitor in monitors.elements_mut() {
if monitor.workspaces().is_empty() {
let mut workspace = Workspace::default();
let monitor_id = monitor.id();
if let Some(workspace) = monitor.workspaces_mut().front_mut() {
// EnumWindows will enumerate through windows on all monitors
Self::enum_windows(
windows_callbacks::enum_window,
@@ -197,7 +304,7 @@ impl WindowsApi {
for container in workspace.containers_mut() {
for window in container.windows() {
if Self::monitor_from_window(window.hwnd()) != monitor.id() {
if Self::monitor_from_window(window.hwnd()) != monitor_id {
windows_on_other_monitors.push(window.hwnd().0);
}
}
@@ -206,8 +313,6 @@ impl WindowsApi {
for hwnd in windows_on_other_monitors {
workspace.remove_window(hwnd)?;
}
monitor.workspaces_mut().push_back(workspace);
}
}
@@ -226,6 +331,12 @@ impl WindowsApi {
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
}
pub fn monitor_from_point(point: POINT) -> isize {
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
}
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
let flags = SetWindowPosition::NO_ACTIVATE;
@@ -314,7 +425,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> {
@@ -341,6 +452,14 @@ impl WindowsApi {
Ok(cursor_pos)
}
pub fn window_from_point(point: POINT) -> Result<isize> {
Result::from(WindowsResult::from(unsafe { WindowFromPoint(point) }))
}
pub fn window_at_cursor_pos() -> Result<isize> {
Self::window_from_point(Self::cursor_pos()?)
}
pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> {
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
}
@@ -463,7 +582,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())
}
@@ -546,6 +665,7 @@ impl WindowsApi {
))
}
#[allow(dead_code)]
pub fn system_parameters_info_w(
action: SYSTEM_PARAMETERS_INFO_ACTION,
ui_param: u32,
@@ -557,6 +677,21 @@ impl WindowsApi {
}))
}
#[allow(dead_code)]
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())
}
#[allow(dead_code)]
pub fn enable_focus_follows_mouse() -> Result<()> {
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,
@@ -566,6 +701,7 @@ impl WindowsApi {
)
}
#[allow(dead_code)]
pub fn disable_focus_follows_mouse() -> Result<()> {
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,
@@ -574,4 +710,192 @@ impl WindowsApi {
SPIF_SENDCHANGE,
)
}
#[allow(dead_code)]
pub fn module_handle_w() -> Result<HINSTANCE> {
Result::from(WindowsResult::from(unsafe { GetModuleHandleW(None) }))
}
#[allow(dead_code)]
pub fn register_class_ex_w(class: &WNDCLASSEXW) -> Result<u16> {
Result::from(WindowsResult::from(unsafe { RegisterClassExW(class) }))
}
#[allow(clippy::too_many_arguments, dead_code)]
fn create_window_ex_w(
window_ex_style: WINDOW_EX_STYLE,
class_name: PWSTR,
window_name: PWSTR,
window_style: WINDOW_STYLE,
x: i32,
y: i32,
width: i32,
height: i32,
hwnd_parent: HWND,
hmenu: HMENU,
hinstance: HINSTANCE,
lp_param: *mut c_void,
) -> Result<isize> {
Result::from(WindowsResult::from(unsafe {
CreateWindowExW(
window_ex_style,
class_name,
window_name,
window_style,
x,
y,
width,
height,
hwnd_parent,
hmenu,
hinstance,
lp_param,
)
}))
}
#[allow(dead_code)]
pub fn hidden_message_window(name: &str, wnd_proc: Option<WNDPROC>) -> Result<isize> {
let hinstance = Self::module_handle_w()?;
let window_class = WNDCLASSEXW {
cbSize: u32::try_from(std::mem::size_of::<WNDCLASSEXW>())?,
cbClsExtra: 0,
cbWndExtra: 0,
hbrBackground: HBRUSH::NULL,
hCursor: HCURSOR::NULL,
hIcon: HICON::NULL,
hIconSm: HICON::NULL,
hInstance: hinstance,
lpfnWndProc: wnd_proc,
lpszClassName: name.into_pwstr(),
lpszMenuName: PWSTR::NULL,
style: WNDCLASS_STYLES::from(0),
};
Self::register_class_ex_w(&window_class)?;
Self::create_window_ex_w(
WINDOW_EX_STYLE::from(0),
name.into_pwstr(),
name.into_pwstr(),
WINDOW_STYLE::from(0),
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
HWND_MESSAGE,
HMENU::NULL,
hinstance,
std::ptr::null_mut(),
)
}
#[allow(dead_code)]
pub fn destroy_window(hwnd: isize) -> Result<()> {
Result::from(WindowsResult::from(unsafe { DestroyWindow(HWND(hwnd)) }))
}
#[allow(dead_code)]
pub fn unregister_class_w(name: &str) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
UnregisterClassW(name.into_pwstr(), Self::module_handle_w()?)
}))
}
#[allow(dead_code)]
pub fn register_raw_input_devices(devices: &mut [RAWINPUTDEVICE]) -> Result<()> {
Result::from(WindowsResult::from(unsafe {
RegisterRawInputDevices(
devices.as_mut_ptr(),
u32::try_from(devices.len())?,
u32::try_from(std::mem::size_of::<RAWINPUTDEVICE>())?,
)
}))
}
#[allow(dead_code)]
pub fn register_mice_for_hwnd(hwnd: isize) -> Result<()> {
Self::register_raw_input_devices(&mut [RAWINPUTDEVICE {
dwFlags: RIDEV_NOLEGACY | RIDEV_INPUTSINK,
usUsagePage: HID_USAGE_PAGE_GENERIC,
usUsage: HID_USAGE_GENERIC_MOUSE,
hwndTarget: HWND(hwnd),
}])
}
#[allow(dead_code)]
pub fn raw_input_buffer_null(buffer_size: *mut u32, header_size: u32) -> Result<()> {
Result::from(unsafe {
match GetRawInputBuffer(std::ptr::null_mut(), buffer_size, header_size) {
0 => WindowsResult::Ok(()),
_ => WindowsResult::Err(std::io::Error::last_os_error().into()),
}
})
}
#[allow(dead_code)]
pub fn raw_input_buffer(
raw_input_pointer: *mut RAWINPUT,
buffer_size: *mut u32,
header_size: u32,
) -> Result<u32> {
Result::from(unsafe {
WindowsResult::Ok(GetRawInputBuffer(
raw_input_pointer,
buffer_size,
header_size,
))
})
}
#[allow(dead_code)]
pub fn raw_input_data_null(raw_input_handle: HRAWINPUT, buffer_size: &mut u32) -> Result<()> {
Result::from(unsafe {
match GetRawInputData(
raw_input_handle,
RID_INPUT,
std::ptr::null_mut(),
buffer_size,
u32::try_from(std::mem::size_of::<RAWINPUTHEADER>())?,
) {
0 => WindowsResult::Ok(()),
_ => WindowsResult::Err(std::io::Error::last_os_error().into()),
}
})
}
#[allow(dead_code)]
pub fn raw_input_data(
raw_input_handle: HRAWINPUT,
buffer: *mut c_void,
buffer_size: *mut u32,
) -> Result<u32> {
Result::from(unsafe {
match GetRawInputData(
raw_input_handle,
RID_INPUT,
buffer,
buffer_size,
u32::try_from(std::mem::size_of::<RAWINPUTHEADER>())?,
) {
0 => WindowsResult::Err(std::io::Error::last_os_error().into()),
n => WindowsResult::Ok(n),
}
})
}
#[allow(dead_code)]
pub fn find_window_ex_w(parent: HWND, class: &str, title: &str) -> Result<isize> {
Result::from(WindowsResult::from(unsafe {
let hwnd = FindWindowExW(parent, HWND::NULL, class.into_pwstr(), title.into_pwstr());
dbg!(hwnd);
hwnd
}))
}
#[allow(dead_code)]
pub fn find_message_window(class: &str, title: &str) -> Result<isize> {
Self::find_window_ex_w(HWND_MESSAGE, class, title)
}
}

View File

@@ -16,6 +16,17 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
monitors.push(hmonitor.0);
true.into()
}
pub extern "system" fn enum_display_monitor(
hmonitor: HMONITOR,
_: HDC,
@@ -23,6 +34,14 @@ pub extern "system" fn enum_display_monitor(
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
// Don't duplicate a monitor that is already being managed
for monitor in monitors.elements() {
if monitor.id() == hmonitor.0 {
return true.into();
}
}
if let Ok(m) = WindowsApi::monitor(hmonitor) {
monitors.elements_mut().push_back(m);
}
@@ -30,6 +49,13 @@ pub extern "system" fn enum_display_monitor(
true.into()
}
#[allow(dead_code)]
pub extern "system" fn valid_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
hwnds.push(hwnd.0);
true.into()
}
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };

View File

@@ -89,7 +89,7 @@ impl MessageLoop {
loop {
let mut value: Option<MSG> = None;
unsafe {
if !bool::from(!PeekMessageW(&mut msg, HWND(0), 0, 0, PM_REMOVE)) {
if bool::from(PeekMessageW(&mut msg, HWND(0), 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);

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;
@@ -138,7 +139,7 @@ impl Workspace {
Ok(())
}
pub fn update(&mut self, work_area: &Rect) -> Result<()> {
pub fn update(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
let mut adjusted_work_area = *work_area;
adjusted_work_area.add_padding(self.workspace_padding());
@@ -147,7 +148,7 @@ impl Workspace {
if *self.tile() {
if let Some(container) = self.monocle_container_mut() {
if let Some(window) = container.focused_window_mut() {
window.set_position(&adjusted_work_area, true)?;
window.set_position(&adjusted_work_area, invisible_borders, true)?;
};
} else if let Some(window) = self.maximized_window_mut() {
window.maximize();
@@ -165,7 +166,7 @@ impl Workspace {
let windows = self.visible_windows_mut();
for (i, window) in windows.into_iter().enumerate() {
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
window.set_position(layout, false)?;
window.set_position(layout, invisible_borders, false)?;
}
}
@@ -229,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);
@@ -262,6 +263,38 @@ impl Workspace {
idx
}
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
for container in self.containers() {
if let Some(hwnd) = container.hwnd_from_exe(exe) {
return Option::from(hwnd);
}
}
if let Some(window) = self.maximized_window() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(window.hwnd);
}
}
}
if let Some(container) = self.monocle_container() {
if let Some(hwnd) = container.hwnd_from_exe(exe) {
return Option::from(hwnd);
}
}
for window in self.floating_windows() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(window.hwnd);
}
}
}
None
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {
@@ -293,7 +326,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);
@@ -328,29 +361,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() {
@@ -393,11 +453,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() {
@@ -417,13 +477,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(())
@@ -434,11 +494,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);
@@ -458,7 +518,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);
@@ -498,11 +558,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);
@@ -547,7 +607,7 @@ 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
@@ -559,7 +619,7 @@ impl Workspace {
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(())
@@ -568,12 +628,12 @@ impl Workspace {
pub fn reintegrate_monocle_container(&mut self) -> Result<()> {
let restore_idx = self
.monocle_container_restore_idx()
.context("there is no monocle restore index")?;
.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 {
@@ -584,7 +644,7 @@ 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);
@@ -598,11 +658,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);
@@ -626,12 +686,12 @@ impl Workspace {
pub fn reintegrate_maximized_window(&mut self) -> Result<()> {
let restore_idx = self
.maximized_window_restore_idx()
.context("there is no monocle restore index")?;
.ok_or_else(|| anyhow!("there is no monocle restore index"))?;
let window = self
.maximized_window()
.as_ref()
.context("there is no monocle container")?;
.ok_or_else(|| anyhow!("there is no monocle container"))?;
let window = *window;
if !self.containers().is_empty() && restore_idx > self.containers().len() - 1 {
@@ -646,7 +706,7 @@ impl Workspace {
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_maximized_window(None);

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

@@ -0,0 +1,197 @@
; Generated by komorebic.exe
Start(ffm) {
Run, komorebic.exe start --ffm %ffm%, , Hide
}
Stop() {
Run, komorebic.exe stop, , Hide
}
State() {
Run, komorebic.exe state, , Hide
}
Query(state_query) {
Run, komorebic.exe query %state_query%, , 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
}
InvisibleBorders(left, top, right, bottom) {
Run, komorebic.exe invisible-borders %left% %top% %right% %bottom%, , 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
}
IdentifyBorderOverflow(identifier, id) {
Run, komorebic.exe identify-border-overflow %identifier% %id%, , Hide
}
FocusFollowsMouse(boolean_state, implementation) {
Run, komorebic.exe focus-follows-mouse %boolean_state% --implementation %implementation%, , Hide
}
ToggleFocusFollowsMouse(implementation) {
Run, komorebic.exe toggle-focus-follows-mouse --implementation %implementation%, , Hide
}
AhkLibrary() {
Run, komorebic.exe ahk-library, , Hide
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.1"
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"]
@@ -12,12 +12,14 @@ edition = "2018"
[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"] }

View File

@@ -2,6 +2,7 @@
#![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;
@@ -15,6 +16,7 @@ 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;
@@ -23,13 +25,26 @@ 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::FocusFollowsMouseImplementation;
use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::SocketMessage;
use komorebi_core::StateQuery;
trait AhkLibrary {
fn generate_ahk_library() -> String;
}
trait AhkFunction {
fn generate_ahk_function() -> String;
}
#[derive(ArgEnum)]
enum BooleanState {
@@ -48,10 +63,10 @@ impl From<BooleanState> for bool {
macro_rules! gen_enum_subcommand_args {
// SubCommand Pattern: Enum Type
( $( $name:ident: $element:ty ),+ ) => {
( $( $name:ident: $element:ty ),+ $(,)? ) => {
$(
paste! {
#[derive(clap::Clap)]
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
#[clap(arg_enum)]
[<$element:snake>]: $element
@@ -67,16 +82,16 @@ gen_enum_subcommand_args! {
Stack: OperationDirection,
CycleStack: CycleDirection,
FlipLayout: Flip,
SetLayout: Layout,
ChangeLayout: Layout,
WatchConfiguration: BooleanState,
FocusFollowsMouse: BooleanState
Query: StateQuery,
}
macro_rules! gen_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Clap)]
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
/// Target index (zero-indexed)
target: usize,
@@ -88,8 +103,10 @@ macro_rules! gen_target_subcommand_args {
gen_target_subcommand_args! {
MoveToMonitor,
MoveToWorkspace,
SendToMonitor,
SendToWorkspace,
FocusMonitor,
FocusWorkspace
FocusWorkspace,
}
// Thanks to @danielhenrymantilla for showing me how to use cfg_attr with an optional argument like
@@ -97,10 +114,10 @@ gen_target_subcommand_args! {
macro_rules! gen_workspace_subcommand_args {
// Workspace Property: #[enum] Value Enum (if the value is an Enum)
// Workspace Property: Value Type (if the value is anything else)
( $( $name:ident: $(#[enum] $(@$arg_enum:tt)?)? $value:ty ),+ ) => (
( $( $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,
@@ -123,10 +140,10 @@ macro_rules! gen_workspace_subcommand_args {
gen_workspace_subcommand_args! {
Name: String,
Layout: #[enum] Layout,
Tiling: #[enum] BooleanState
Tiling: #[enum] BooleanState,
}
#[derive(Clap)]
#[derive(Clap, AhkFunction)]
struct Resize {
#[clap(arg_enum)]
edge: OperationDirection,
@@ -134,7 +151,19 @@ struct Resize {
sizing: Sizing,
}
#[derive(Clap)]
#[derive(Clap, AhkFunction)]
struct InvisibleBorders {
/// Size of the left invisible border
left: i32,
/// Size of the top invisible border (usually 0)
top: i32,
/// Size of the right invisible border (usually left * 2)
right: i32,
/// Size of the bottom invisible border (usually the same as left)
bottom: i32,
}
#[derive(Clap, AhkFunction)]
struct EnsureWorkspaces {
/// Monitor index (zero-indexed)
monitor: usize,
@@ -142,33 +171,71 @@ 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 {
#[clap(arg_enum)]
identifier: ApplicationIdentifier,
/// Identifier as a string
id: String,
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,
}
)+
};
}
#[derive(Clap)]
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,
IdentifyBorderOverflow,
}
#[derive(Clap, AhkFunction)]
struct WorkspaceRule {
#[clap(arg_enum)]
identifier: ApplicationIdentifier,
@@ -180,6 +247,27 @@ struct WorkspaceRule {
workspace: usize,
}
#[derive(Clap, AhkFunction)]
struct ToggleFocusFollowsMouse {
#[clap(arg_enum, short, long, default_value = "windows")]
implementation: FocusFollowsMouseImplementation,
}
#[derive(Clap, AhkFunction)]
struct FocusFollowsMouse {
#[clap(arg_enum, short, long, default_value = "windows")]
implementation: FocusFollowsMouseImplementation,
#[clap(arg_enum)]
boolean_state: BooleanState,
}
#[derive(Clap, AhkFunction)]
struct Start {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(long)]
ffm: bool,
}
#[derive(Clap)]
#[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)]
struct Opts {
@@ -187,14 +275,17 @@ struct Opts {
subcmd: SubCommand,
}
#[derive(Clap)]
#[derive(Clap, AhkLibrary)]
enum SubCommand {
/// Start komorebi.exe as a background process
Start,
Start(Start),
/// Stop the komorebi.exe process and restore all hidden windows
Stop,
/// Show a JSON representation of the current window manager state
State,
/// Query the current window manager state
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
Query(Query),
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
Log,
/// Change focus to the window in the specified direction
@@ -220,6 +311,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),
@@ -228,15 +325,20 @@ enum SubCommand {
FocusWorkspace(FocusWorkspace),
/// Create and append a new workspace on the focused monitor
NewWorkspace,
/// Set the invisible border dimensions around each window
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
InvisibleBorders(InvisibleBorders),
/// 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
ChangeLayout(SetLayout),
#[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,
@@ -247,10 +349,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),
@@ -278,23 +380,32 @@ enum SubCommand {
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(ApplicationTarget),
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),
/// Identify an application that has overflowing borders
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
IdentifyBorderOverflow(IdentifyBorderOverflow),
/// Enable or disable focus follows mouse for the operating system
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
FocusFollowsMouse(FocusFollowsMouse),
/// Toggle focus follows mouse for the operating system
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ToggleFocusFollowsMouse(ToggleFocusFollowsMouse),
/// Generate a library of AutoHotKey helper functions
AhkLibrary,
}
pub fn send_message(bytes: &[u8]) -> Result<()> {
@@ -311,6 +422,30 @@ 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");
@@ -341,6 +476,23 @@ 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::InvisibleBorders(arg) => {
send_message(
&*SocketMessage::InvisibleBorders(Rect {
left: arg.left,
top: arg.top,
right: arg.right,
bottom: arg.bottom,
})
.as_bytes()?,
)?;
}
SubCommand::ContainerPadding(arg) => {
send_message(
&*SocketMessage::ContainerPadding(arg.monitor, arg.workspace, arg.size)
@@ -363,6 +515,9 @@ fn main() -> Result<()> {
&*SocketMessage::AdjustContainerPadding(arg.sizing, arg.adjustment).as_bytes()?,
)?;
}
SubCommand::ToggleFocusFollowsMouse(arg) => {
send_message(&*SocketMessage::ToggleFocusFollowsMouse(arg.implementation).as_bytes()?)?;
}
SubCommand::ToggleTiling => {
send_message(&*SocketMessage::ToggleTiling.as_bytes()?)?;
}
@@ -387,7 +542,7 @@ fn main() -> Result<()> {
.as_bytes()?,
)?;
}
SubCommand::Start => {
SubCommand::Start(arg) => {
let mut buf: PathBuf;
// The komorebi.ps1 shim will only exist in the Path if installed by Scoop
@@ -410,11 +565,27 @@ fn main() -> Result<()> {
None
};
let script = if let Some(exec) = exec {
format!("Start-Process '{}' -WindowStyle hidden", exec)
} else {
String::from("Start-Process komorebi -WindowStyle hidden")
};
let script = exec.map_or_else(
|| {
if arg.ffm {
String::from(
"Start-Process komorebi.exe -ArgumentList '--ffm' -WindowStyle hidden",
)
} else {
String::from("Start-Process komorebi.exe -WindowStyle hidden")
}
},
|exec| {
if arg.ffm {
format!(
"Start-Process '{}' -ArgumentList '--ffm' -WindowStyle hidden",
exec
)
} else {
format!("Start-Process '{}' -WindowStyle hidden", exec)
}
},
);
match powershell_script::run(&script, true) {
Ok(output) => {
@@ -510,6 +681,40 @@ fn main() -> Result<()> {
}
}
}
SubCommand::Query(arg) => {
let home = dirs::home_dir().context("there is no home directory")?;
let mut socket = home;
socket.push("komorebic.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());
}
},
};
send_message(&*SocketMessage::Query(arg.state_query).as_bytes()?)?;
let listener = UnixListener::bind(&socket)?;
match listener.accept() {
Ok(incoming) => {
let stream = BufReader::new(incoming.0);
for line in stream.lines() {
println!("{}", line?);
}
return Ok(());
}
Err(error) => {
panic!("{}", error);
}
}
}
SubCommand::RestoreWindows => {
let mut hwnd_json = dirs::home_dir().context("there is no home directory")?;
hwnd_json.push("komorebi.hwnd.json");
@@ -531,7 +736,9 @@ fn main() -> Result<()> {
BooleanState::Disable => false,
};
send_message(&*SocketMessage::FocusFollowsMouse(enable).as_bytes()?)?;
send_message(
&*SocketMessage::FocusFollowsMouse(arg.implementation, enable).as_bytes()?,
)?;
}
SubCommand::ReloadConfiguration => {
send_message(&*SocketMessage::ReloadConfiguration.as_bytes()?)?;
@@ -549,6 +756,11 @@ fn main() -> Result<()> {
.as_bytes()?,
)?;
}
SubCommand::IdentifyBorderOverflow(target) => {
send_message(
&*SocketMessage::IdentifyBorderOverflow(target.identifier, target.id).as_bytes()?,
)?;
}
SubCommand::Manage => {
send_message(&*SocketMessage::ManageFocusedWindow.as_bytes()?)?;
}