Compare commits

..

22 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
25 changed files with 1327 additions and 305 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

97
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"
@@ -310,7 +310,7 @@ checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall 0.2.10",
"redox_syscall",
"winapi 0.3.9",
]
@@ -509,6 +509,7 @@ version = "0.1.3"
dependencies = [
"bindings",
"bitflags",
"clap",
"color-eyre",
"crossbeam-channel",
"crossbeam-utils",
@@ -530,6 +531,7 @@ dependencies = [
"tracing-subscriber",
"uds_windows",
"which",
"winput",
"winvd",
]
@@ -578,15 +580,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.100"
version = "0.2.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5"
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",
]
@@ -768,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",
]
@@ -795,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",
@@ -806,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",
@@ -881,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",
]
@@ -999,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"
@@ -1021,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]]
@@ -1059,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"
@@ -1086,18 +1082,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.128"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1056a0db1978e9dbf0f6e4fca677f6f9143dc1c19de346f22cac23e422196834"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.128"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13af2fbb8b60a8950d6c72a56d2095c28870367cc8e10c55e9745bac4995a2c4"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [
"proc-macro2",
"quote",
@@ -1106,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",
@@ -1165,9 +1161,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.75"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7"
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
dependencies = [
"proc-macro2",
"quote",
@@ -1176,9 +1172,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.20.1"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4786f320388e6031aa78c68455553f4e3425deeeb40565fecba3d101c1faf21f"
checksum = "92d77883450d697c0010e60db3d940ed130b0ed81d27485edee981621b434e52"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
@@ -1219,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",
]
@@ -1249,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",
@@ -1272,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",
@@ -1283,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",
]
@@ -1323,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",
@@ -1499,6 +1495,15 @@ 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]]
name = "winvd"
version = "0.0.20"

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
@@ -174,6 +198,7 @@ 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
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
@@ -188,6 +213,7 @@ 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
@@ -214,6 +240,7 @@ float-rule Add a rule to always float the specified applicati
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
@@ -254,11 +281,14 @@ used [is available here](komorebi.sample.with.lib.ahk).
- [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
@@ -266,6 +296,7 @@ used [is available here](komorebi.sample.with.lib.ahk).
- [x] Watch configuration for changes
- [x] Helper library for AutoHotKey
- [x] View window manager state
- [x] Query window manager state
## Development
@@ -314,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

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

View File

@@ -8,11 +8,11 @@ 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 ::std::option::Option::Some;
use ::syn::parse_macro_input;
use ::syn::Data;
use ::syn::DataEnum;
@@ -20,7 +20,10 @@ 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);
@@ -29,29 +32,118 @@ pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStre
match input.data {
Data::Struct(s) => match s.fields {
Fields::Named(FieldsNamed { named, .. }) => {
let idents = named.iter().map(|f| &f.ident);
let arguments = quote! {#(#idents), *}.to_string();
let 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;
}
}
}
}
}
let idents = named.iter().map(|f| &f.ident);
let called_arguments = quote! {#(%#idents%) *}
include
})
.map(|f| &f.ident);
let argument_idents_clone = argument_idents.clone();
let called_arguments = quote! {#(%#argument_idents_clone%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
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,
stringify!(#name).to_kebab_case(),
#called_arguments
)
}
::std::stringify!(#name),
#arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments
)
}
}
}
}
}

View File

@@ -62,13 +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),
ToggleFocusFollowsMouse,
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
}
impl SocketMessage {
@@ -89,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 {
@@ -97,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

@@ -7,6 +7,9 @@ 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")
@@ -40,6 +43,12 @@ 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::
@@ -170,9 +179,9 @@ return
TogglePause()
return
; Toggle focus follows mouse
; Enable focus follows mouse
!0::
ToggleFocusFollowsMouse()
ToggleFocusFollowsMouse("komorebi")
return
; Switch to workspace

View File

@@ -1,6 +1,11 @@
[package]
name = "komorebi"
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,6 +15,7 @@ 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"
@@ -30,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,12 +3,15 @@
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 clap::Clap;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_channel::Receiver;
@@ -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");
@@ -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

@@ -6,6 +6,7 @@ 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(),
}
}
@@ -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()
.ok_or_else(|| anyhow!("there is no workspace"))?
.update(&work_area)?;
.update(&work_area, invisible_borders)?;
Ok(())
}

View File

@@ -2,6 +2,7 @@ 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;
@@ -10,16 +11,18 @@ 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]
@@ -113,7 +116,12 @@ impl WindowManager {
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 => {
@@ -123,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()
.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)?;
}
}
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) => {
@@ -147,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 => {
@@ -175,21 +178,124 @@ impl WindowManager {
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 => {
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
} else {
WindowsApi::enable_focus_follows_mouse()?;
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 => {
@@ -198,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(())
@@ -232,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

@@ -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,8 +54,9 @@ impl WindowManager {
WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::MoveResizeEnd(_, window) => {
let monitor_idx = self
.monitor_idx_from_window(*window)
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)?;
@@ -64,12 +64,14 @@ impl WindowManager {
_ => {}
}
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;
@@ -213,19 +220,11 @@ impl WindowManager {
.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");
@@ -315,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

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

View File

@@ -17,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;
@@ -31,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)]
@@ -44,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)]
@@ -65,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(),
}
}
}
@@ -128,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,
})
}
@@ -215,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![];
@@ -332,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();
@@ -360,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()
.ok_or_else(|| anyhow!("there is no monitor"))?
.update_focused_workspace()?;
.update_focused_workspace(&invisible_borders)?;
if mouse_follows_focus {
if let Some(window) = self.focused_workspace()?.maximized_window() {
@@ -488,6 +627,8 @@ impl WindowManager {
pub fn move_container_to_monitor(&mut self, idx: usize, follow: bool) -> Result<()> {
tracing::info!("moving container");
let invisible_borders = self.invisible_borders;
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
@@ -512,6 +653,7 @@ impl WindowManager {
target_monitor.add_container(container)?;
target_monitor.load_focused_workspace()?;
target_monitor.update_focused_workspace(&invisible_borders)?;
if follow {
self.focus_monitor(idx)?;
@@ -673,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()?;
@@ -682,7 +825,7 @@ impl WindowManager {
.last_mut()
.ok_or_else(|| anyhow!("there is no floating window"))?;
window.center(&work_area)?;
window.center(&work_area, &invisible_borders)?;
window.focus()?;
Ok(())
@@ -865,6 +1008,7 @@ 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
@@ -884,7 +1028,7 @@ impl WindowManager {
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
workspace.update(&work_area)?;
workspace.update(&work_area, &invisible_borders)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false)?)
@@ -1014,6 +1158,18 @@ 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()
.ok_or_else(|| anyhow!("there is no monitor"))?
@@ -1068,6 +1224,12 @@ impl WindowManager {
.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()

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

@@ -7,8 +7,11 @@ use color_eyre::eyre::anyhow;
use color_eyre::eyre::Error;
use color_eyre::Result;
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,15 +72,23 @@ 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;
@@ -74,8 +101,13 @@ 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;
@@ -84,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),
@@ -101,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() {
@@ -136,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 {
@@ -164,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,
@@ -179,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,
@@ -198,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);
}
}
@@ -207,8 +313,6 @@ impl WindowsApi {
for hwnd in windows_on_other_monitors {
workspace.remove_window(hwnd)?;
}
monitor.workspaces_mut().push_back(workspace);
}
}
@@ -227,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;
@@ -342,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))
}
@@ -547,6 +665,7 @@ impl WindowsApi {
))
}
#[allow(dead_code)]
pub fn system_parameters_info_w(
action: SYSTEM_PARAMETERS_INFO_ACTION,
ui_param: u32,
@@ -558,6 +677,7 @@ impl WindowsApi {
}))
}
#[allow(dead_code)]
pub fn focus_follows_mouse() -> Result<bool> {
let mut is_enabled: BOOL = unsafe { std::mem::zeroed() };
@@ -571,6 +691,7 @@ impl WindowsApi {
Ok(is_enabled.into())
}
#[allow(dead_code)]
pub fn enable_focus_follows_mouse() -> Result<()> {
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,
@@ -580,6 +701,7 @@ impl WindowsApi {
)
}
#[allow(dead_code)]
pub fn disable_focus_follows_mouse() -> Result<()> {
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,
@@ -588,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

@@ -139,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());
@@ -148,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();
@@ -166,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)?;
}
}
@@ -263,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) {

View File

@@ -1,7 +1,7 @@
; Generated by komorebic.exe
Start() {
Run, komorebic.exe start, , Hide
Start(ffm) {
Run, komorebic.exe start --ffm %ffm%, , Hide
}
Stop() {
@@ -12,6 +12,10 @@ State() {
Run, komorebic.exe state, , Hide
}
Query(state_query) {
Run, komorebic.exe query %state_query%, , Hide
}
Log() {
Run, komorebic.exe log, , Hide
}
@@ -68,6 +72,10 @@ 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
}
@@ -172,12 +180,16 @@ IdentifyTrayApplication(identifier, id) {
Run, komorebic.exe identify-tray-application %identifier% %id%, , Hide
}
FocusFollowsMouse(boolean_state) {
Run, komorebic.exe focus-follows-mouse %boolean_state%, , Hide
IdentifyBorderOverflow(identifier, id) {
Run, komorebic.exe identify-border-overflow %identifier% %id%, , Hide
}
ToggleFocusFollowsMouse() {
Run, komorebic.exe toggle-focus-follows-mouse, , 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() {

View File

@@ -9,7 +9,6 @@ use std::io::ErrorKind;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::stringify;
use clap::AppSettings;
use clap::ArgEnum;
@@ -31,10 +30,13 @@ 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;
@@ -61,7 +63,7 @@ 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_ahk::AhkFunction)]
@@ -82,12 +84,12 @@ gen_enum_subcommand_args! {
FlipLayout: Flip,
ChangeLayout: Layout,
WatchConfiguration: BooleanState,
FocusFollowsMouse: BooleanState
Query: StateQuery,
}
macro_rules! gen_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
@@ -104,7 +106,7 @@ gen_target_subcommand_args! {
SendToMonitor,
SendToWorkspace,
FocusMonitor,
FocusWorkspace
FocusWorkspace,
}
// Thanks to @danielhenrymantilla for showing me how to use cfg_attr with an optional argument like
@@ -112,7 +114,7 @@ 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_ahk::AhkFunction)]
@@ -138,7 +140,7 @@ macro_rules! gen_workspace_subcommand_args {
gen_workspace_subcommand_args! {
Name: String,
Layout: #[enum] Layout,
Tiling: #[enum] BooleanState
Tiling: #[enum] BooleanState,
}
#[derive(Clap, AhkFunction)]
@@ -149,6 +151,18 @@ struct Resize {
sizing: Sizing,
}
#[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)
@@ -159,7 +173,7 @@ struct EnsureWorkspaces {
macro_rules! gen_padding_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
@@ -176,12 +190,12 @@ macro_rules! gen_padding_subcommand_args {
gen_padding_subcommand_args! {
ContainerPadding,
WorkspacePadding
WorkspacePadding,
}
macro_rules! gen_padding_adjustment_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
@@ -196,12 +210,12 @@ macro_rules! gen_padding_adjustment_subcommand_args {
gen_padding_adjustment_subcommand_args! {
AdjustContainerPadding,
AdjustWorkspacePadding
AdjustWorkspacePadding,
}
macro_rules! gen_application_target_subcommand_args {
// SubCommand Pattern
( $( $name:ident ),+ ) => {
( $( $name:ident ),+ $(,)? ) => {
$(
#[derive(clap::Clap, derive_ahk::AhkFunction)]
pub struct $name {
@@ -217,7 +231,8 @@ macro_rules! gen_application_target_subcommand_args {
gen_application_target_subcommand_args! {
FloatRule,
ManageRule,
IdentifyTrayApplication
IdentifyTrayApplication,
IdentifyBorderOverflow,
}
#[derive(Clap, AhkFunction)]
@@ -232,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 {
@@ -242,11 +278,14 @@ struct Opts {
#[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
@@ -286,6 +325,9 @@ 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(AdjustContainerPadding),
@@ -353,11 +395,15 @@ enum SubCommand {
/// Identify an application that closes to the system tray
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
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
ToggleFocusFollowsMouse,
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
ToggleFocusFollowsMouse(ToggleFocusFollowsMouse),
/// Generate a library of AutoHotKey helper functions
AhkLibrary,
}
@@ -436,6 +482,17 @@ fn main() -> Result<()> {
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)
@@ -458,8 +515,8 @@ fn main() -> Result<()> {
&*SocketMessage::AdjustContainerPadding(arg.sizing, arg.adjustment).as_bytes()?,
)?;
}
SubCommand::ToggleFocusFollowsMouse => {
send_message(&*SocketMessage::ToggleFocusFollowsMouse.as_bytes()?)?;
SubCommand::ToggleFocusFollowsMouse(arg) => {
send_message(&*SocketMessage::ToggleFocusFollowsMouse(arg.implementation).as_bytes()?)?;
}
SubCommand::ToggleTiling => {
send_message(&*SocketMessage::ToggleTiling.as_bytes()?)?;
@@ -485,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
@@ -508,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) => {
@@ -608,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");
@@ -629,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()?)?;
@@ -647,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()?)?;
}