Compare commits

...

50 Commits

Author SHA1 Message Date
LGUG2Z
8176849dc3 fix(wm): restart cmd processing thread on fail
This commit makes some changes to process_command and process_event to
bring them more in line with newer modules, while primarily making sure
that the thread spawned in process_command will be restarted if
necessary.

In the future I might pull out the process_command and process_event fns
from the WindowManager struct and have them take an
Arc<Mutex<WindowManager>> instead which acquires the lock on every call
like the reconciliators and border_manager do.
2024-05-15 09:51:57 -07:00
LGUG2Z
228cb26b64 fix(wm): remove object name change spam guard
The spam guard removed in this commit is no longer needed after commit
54c58be.
2024-05-15 09:51:57 -07:00
LGUG2Z
3d53c602a7 feat(wm): single_window -> window_based offset
This commit updates the initial design of single_window_work_area_offset
to window_based_work_area_offset where the user can set
window_based_work_area_offset_limit to determine the limit of windows on
the screen that this offset should apply to.

By default this is 1, and in the most extreme case of someone using a
super ultrawide monitor this might be 2.
2024-05-15 09:51:57 -07:00
LGUG2Z
81f741bbbd fix(wm): always respect adjusted work area
This commit fixes a regression which led to the adjusted work area for a
monitor not being respected when applying additional window-based work
area offsets.

re #811
2024-05-15 09:51:57 -07:00
LGUG2Z
0330dfe250 feat(wm): add single window work area offsets
This commit adds a new monitor configuration option, single_window_work_area_offset, which will
apply to a monitor when only a single window is open on a workspace. This is implemented as a Rect
to enable its use on both horizontally and vertically positioned monitors. This option will be
particularly useful for ultrawide monitor users, where a single window taking up the full width of
the work area can often hinder usability.

resolve #434
2024-05-15 09:51:56 -07:00
LGUG2Z
70ef90b304 perf(wm): compare rects before position updates
This commit ensures that both windows and borders will make a comparison
to the current values returned by WindowRect before attempting to make
update calls to SetWindowPos. This should greatly reduce visual
flickering in both the borders and sensitive apps like JetBrains IDEs.
2024-05-15 09:51:56 -07:00
LGUG2Z
d102c00ffe feat(wm): add alt-tab heuristics to wsr
This commit adds some rough heuristics to workspace_reconciliator which
should help with having the correct window focused after reconciliation
in the majority of, but probably not all, cases.

EnumWindows generally returns HWNDs according to z order, and a window
selected by alt-tab will almost always be put on the top of the z order.
Before sending a workspace_reconciliator::Notification, we store this
HWND along with an Instant and an AtomicBool telling us that we have a
candidate to focus after the workspace switch.
2024-05-15 09:51:56 -07:00
LGUG2Z
87b1ab9c53 fix(wm): restart bm and wsr notif handlers on fail
This commit ensures that if and when either the border_manager or
workspace_reconciliator notification handlers crash in their respective
threads, they will be restarted instead of killing the whole thread.

This is acheived via a loop running in the spawned threads of both
listeners, which will report whichever error killed the notification
handler functions.

Clippy annotations to deny expect and unwrap calls have been added to
these two modules.

Calls to DestroyWindow for expired borders were sporadically failing in
unpredictable circumstances, so these have been switched out with calls
to CloseWindow which has been working well so far with the stackbar
feature.
2024-05-15 09:51:56 -07:00
LGUG2Z
a4dd5fc741 refactor(clippy): apply lints 2024-05-15 09:51:56 -07:00
LGUG2Z
226ee73aa4 fix(borders): reap untracked hwnds in destroy_all
This commit ensures that even border hwnds that may now be untracked as
a result of events such as monitor changes will now be reaped in the
destroy_all_borders function.
2024-05-15 09:51:56 -07:00
LGUG2Z
e14235c3a9 perf(wm): ignore same-workspace switch requests
This commit ensures that if a user uses index-based commands to switch
workspaces, workspace layout update code paths will not be run if the
user is already on the desired monitor and workspace indices.

resolve #647
2024-05-15 09:51:56 -07:00
LGUG2Z
855bb49804 feat(wm): add noop cross-monitor-move-behaviour
This commit adds a new "NoOp" MoveBehaviour for users who don't want any
moves to happen across monitor boundaries. The
toggle-cross-monitor-move-behaviour will only toggle between Swap and
Insert, and will do nothing if NoOp is the selected MoveBehaviour.

resolve #667
2024-05-15 09:51:56 -07:00
LGUG2Z
07b2da69a1 feat(wm): add workspace reconciliator module
This commit adds the workspace_reconciliator module which uses a tightly
bounded channel (cap: 1) to update the focused workspace in situations
where the user or another process has foregrounded a window that is on a
different workspace.

This most often happens via Alt-Tab, or by clicking a link which opens
in another application.

workspace_reconciliator::Notification contains the target monitor and
workspace indices, which when received allows for the correct workspace to
be focused.

These notifications are sent in process_event.rs when handling
WindowManagerEvent::Show, Manage and Uncloak events.

All previous logic pertaining to workspace reconciliation which lived in
the handler for these events has been removed and replaced with
notifications sent to the reconciliator.

As the notifications channel is tightly capped and any notifications
which overflow the cap will never be delivered, we are able to avoid the
infinite workspace switching loops which happened when using the
previous logic, which ran on every single event.
2024-05-12 09:14:55 -07:00
LGUG2Z
6a1ed3bcaa feat(borders): add border manager module
This commit removes all border-related code from process_command,
process_event etc. and centralizes it in the new border_manager module.

Instead of trying to figure out where in process_event and
process_command we should make border-related changes, a notification
gets sent to a channel that border_manager listens to whenever an event
or command has finished processing.

The border_manager listener, upon receiving a notification, acquires a
lock on the WindowManager instance and updates borders for the focused
workspace on every monitor; this allows us to centralize all edge case
handling within the border_manager listener's loop.

Borders on workspaces that lose focus are now destroyed and recreated
when those workspaces regain focus, instead of trying to share
individual border instances across workspaces.

A number of common edge cases that have been addressed in this commit
are:

* Paused window manager
* Floating workspaces
* Maximized windows
* Fullscreen videos
* Monocle containers
* Ghost borders on workspace switching
* Incorrect focused window border colours

Global state related to borders has also been moved into the
border_manager module, which also tracks the state of border objects
(BORDER_STATE), their rects (RECT_STATE) and their focus kinds
(FOCUS_STATE).

This allows us to now track multiple borders per-container, enabling
unfocused border windows for the first time.

Additionally, the Z-Order for border windows is now also configurable.

ActiveWindowBorderColours has been expanded to include Unfocused, but in
order to not introduce a breaking configuration change for end users,
all members of this struct have been made Option<Colour>.
2024-05-12 09:14:50 -07:00
LGUG2Z
1b30561989 fix(wm): update focused idx on cross-monitor moves
This commit ensures that in situations where the last container on a
monitor is moved to an adjacent monitor, the focused container index of
the origin monitor will be appropriately decremented.
2024-05-11 16:47:44 -07:00
LGUG2Z
c47cf4718b docs(mkdocs): add ahk exe troubleshooting section 2024-05-10 14:50:41 -07:00
LGUG2Z
1accbf65ca docs(mkdocs): add shell.nix for python doc deps 2024-05-10 14:39:41 -07:00
0x3bb
7ee3c928d8 docs(mkdocs): addressing display suspension within monitor OSD 2024-05-10 14:39:41 -07:00
0x3bb
6d1903099a docs(mkdocs): add pymdownx.highlight and pymdownx.superfences extensions
Used for syntax highlighting in code blocks, and allows for indenting existing code blocks.

References:
- https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#highlight
- https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#superfences
2024-05-10 14:39:41 -07:00
0x3bb
d5c6f090cc docs(mkdocs): add admonition extension
Used for bringing attention to troubleshooting steps in komorebi documentation.

Reference: https://squidfunk.github.io/mkdocs-material/reference/admonitions/
2024-05-10 14:39:41 -07:00
LGUG2Z
598f9ec0aa fix(wm): avoid dupes when following links
When adding selective handling of Uncloak events, a regression was
introduced where, for example, when clicking a link from Discord on
Workspace 2, a Firefox instance on Workspace 1 would be moved to
Workspace 2 to open the link, but when moving back to Workspace 1, a
ghost tile would be left, and the Firefox instance would be duplicated
across two workspaces.

This commit fixes this regression and makes the handler a bit easier to
reason about while also removing unnecessary early return statements
which prevent notifcations from getting sent to subscribers.
2024-05-09 20:10:38 -07:00
LGUG2Z
11acff5236 refactor(wm): reduce noise in info and error logs 2024-05-09 19:49:43 -07:00
LGUG2Z
4802b55452 feat(wm): selectively handle uncloak events
This commit adds selective handling for WindowManagerEvent::Uncloak
alongside Show and Manage, avoiding the workspace-switching logic that
is known to cause infinite workspace loops.
2024-05-08 08:58:41 -07:00
dependabot[bot]
482a7b1d7f chore(deps): bump schemars from 0.8.17 to 0.8.18
Bumps [schemars](https://github.com/GREsau/schemars) from 0.8.17 to 0.8.18.
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.17...v0.8.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 20:32:36 -07:00
dependabot[bot]
d00ee82a9d chore(deps): bump sysinfo from 0.30.11 to 0.30.12
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.11 to 0.30.12.
- [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>
2024-05-06 20:32:19 -07:00
dependabot[bot]
9f01d8fa0f chore(deps): bump serde from 1.0.199 to 1.0.200
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.199 to 1.0.200.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.199...v1.0.200)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 20:31:51 -07:00
LGUG2Z
627088c9b9 fix(wm): focus workspaces on cross-monitor cmds
This commit ensures that if a window is moved to a workspace on another
monitor which is not the currently focused workspace on that monitor,
the target workspace will be focused as part of the operation.
2024-05-06 18:31:01 -07:00
LGUG2Z
22cf7b5017 docs(readme): add comparison with fancy zones 2024-05-02 15:52:19 -07:00
LGUG2Z
3e984d886c docs(mkdocs): fix header level typo 2024-05-01 17:42:27 -07:00
LGUG2Z
185cb4d4a8 chore(dev): begin v0.1.26-dev 2024-04-30 15:02:48 -07:00
LGUG2Z
62900c59cb chore(release): v0.1.25 2024-04-30 14:49:45 -07:00
MasouShizuka
a2e9a46582 feat(wm): add resize of all layouts except grid with filp
fix: fix horizontal-stack out of monitor with vertical flip (db28e25)

feat: add resize of right-main-vertical-stack layout with filp (28dd546)
2024-04-30 13:09:53 -07:00
dependabot[bot]
871a53821c chore(deps): bump schemars from 0.8.16 to 0.8.17
Bumps [schemars](https://github.com/GREsau/schemars) from 0.8.16 to 0.8.17.
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.16...v0.8.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 18:27:57 -07:00
dependabot[bot]
bcd1c50d82 chore(deps): bump parking_lot from 0.12.1 to 0.12.2
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.12.1 to 0.12.2.
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/0.12.1...0.12.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>
2024-04-29 18:27:44 -07:00
dependabot[bot]
0c41d9ded2 chore(deps): bump serde from 1.0.197 to 1.0.199
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.197 to 1.0.199.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.197...v1.0.199)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 18:27:17 -07:00
dependabot[bot]
4af62fe97b chore(deps): bump thiserror from 1.0.58 to 1.0.59
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.58 to 1.0.59.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.59)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 18:26:10 -07:00
LGUG2Z
7cab062124 fix(stackbar): avoid drops on notification events
This commit completely removes the custom Clone and Drop trait
implementation for Stackbar, and moves the handling to be explicit
within container.rs, which helps us to avoid unintentional drops when
the Stackbar struct is cloned for Notification events sent to
subscribers such as Zebar.

re #746
2024-04-29 17:31:27 -07:00
LGUG2Z
611fa34567 fix(stackbar): avoid drops from variable updates
This commit ensures that when we call methods to change
Container.stackbar we are not unintentionally invoking a Drop which
kills a stackbar window that already exists.

Checks have been added to make sure that we not change the value of the
stackbar variable if it is already an Option::Some.

re #746 re #792
2024-04-29 13:56:24 -07:00
LGUG2Z
95990d682b fix(config): ensure tiling with layout rules
This commit ensures that tiling on a workspace will be enabled if a user
specifies layout rules or custom layout rules without providing an
option to the "layout" key.
2024-04-29 08:06:37 -07:00
LGUG2Z
e363a494c3 docs(mkdocs): various updates to reflect v0.1.25-dev.0 2024-04-28 13:51:08 -07:00
LGUG2Z
383533e2d9 feat(wm): add right-main-vertical-stack layout
This commit adds a new RightMainVerticalStack layout, adapting code from
the similarly named LeftWM layout.

It turns out that the horizontal axis flip on the VerticalStack does not
play well with resize offsets.

It was ultimately easier to implement this layout and the logic for
resizing both VerticalStack and RightMainVerticalStack independently
than to make resize offsets and horizontal axis flips work together.

I still have no idea why resize offsets and horizontal axis flips aren't
working properly together.

Horizontal axis flips have been disabled for both the VerticalStack and
RightMainVerticalStack layouts.

re #789
2024-04-28 12:17:53 -07:00
LGUG2Z
0b04e3ef93 fix(wm): avoid out of range container focus op
This commit ensures that when floating the sole window visible on a
workspace that the focused container index for the workspace will not be
decremented below 0.

re #787
2024-04-25 18:30:52 -07:00
LGUG2Z
3370e6acc5 fix(wm): revert mff edge case regression
This commit ensures that the wm's mouse_follows_mouse state is respected when handling FocusChange
WindowManagerEvents, so that applications opened on empty workspaces do not automatically center the
cursor unless configured to do so.

fix #782
2024-04-23 07:19:53 -07:00
LGUG2Z
4ffffc5eec fix(wm): revert regression in ffm raise handler 2024-04-23 07:03:48 -07:00
dependabot[bot]
2b5f737d14 chore(deps): bump proc-macro2 from 1.0.80 to 1.0.81
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.80 to 1.0.81.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.80...1.0.81)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 17:16:27 -07:00
dependabot[bot]
8a455c8ab7 chore(deps): bump serde_json from 1.0.115 to 1.0.116
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.115 to 1.0.116.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.115...v1.0.116)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 17:16:14 -07:00
dependabot[bot]
3d9871c576 chore(deps): bump reqwest from 0.12.3 to 0.12.4
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.3 to 0.12.4.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.3...v0.12.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 17:15:51 -07:00
dependabot[bot]
cafb8e9a48 chore(deps): bump sysinfo from 0.30.10 to 0.30.11
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.10 to 0.30.11.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.30.10...v0.30.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 17:15:26 -07:00
Sam Vendittelli
740cb3c877 fix(docs): correct typo in example config
The example configuration mistakenly used the key `border_padding` in
the place of `border_width`. As `border_padding` does not exist in the
spec, modifying its value has no effect.

As this file is used by `komorebic quickstart`, new users will have this
incorrect key in their default configuration. Notably, setting its value
to `0` to remove gaps has no effect. The rest of the documentation uses
the correct key, so users copying and pasting from that would not
encounter the bug.
2024-04-21 17:43:13 -07:00
LGUG2Z
b8b3b3d615 docs(wm): add stackbar config struct docs 2024-04-21 14:12:09 -07:00
49 changed files with 2331 additions and 1226 deletions

View File

@@ -10,8 +10,8 @@ before:
builds:
- id: komorebi
main: dummy.go
goos: ["windows"]
goarch: ["amd64"]
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi
hooks:
post:
@@ -19,8 +19,8 @@ builds:
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
- id: komorebic
main: dummy.go
goos: ["windows"]
goarch: ["amd64"]
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic
hooks:
post:
@@ -28,8 +28,8 @@ builds:
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
- id: komorebic-no-console
main: dummy.go
goos: ["windows"]
goarch: ["amd64"]
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic-no-console
hooks:
post:
@@ -40,7 +40,7 @@ archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
format: zip
files:
- LICENSE
- LICENSE.md
- CHANGELOG.md
checksum:

155
Cargo.lock generated
View File

@@ -145,9 +145,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cc"
version = "1.0.94"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7"
checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd"
[[package]]
name = "cfg-if"
@@ -199,7 +199,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -395,9 +395,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.0.2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "file-id"
@@ -416,7 +416,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"redox_syscall 0.4.1",
"windows-sys 0.52.0",
]
@@ -574,9 +574,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.14.3"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
@@ -669,9 +669,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "hyper"
version = "1.3.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f24ce812868d86d19daa79bf3bf9175bc44ea323391147a5e3abde2a283871b"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [
"bytes",
"futures-channel",
@@ -798,7 +798,7 @@ dependencies = [
[[package]]
name = "komorebi"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
dependencies = [
"bitflags 2.5.0",
"clap",
@@ -839,7 +839,7 @@ dependencies = [
[[package]]
name = "komorebi-client"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
dependencies = [
"komorebi",
"komorebi-core",
@@ -849,7 +849,7 @@ dependencies = [
[[package]]
name = "komorebi-core"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
dependencies = [
"clap",
"color-eyre",
@@ -865,7 +865,7 @@ dependencies = [
[[package]]
name = "komorebic"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
dependencies = [
"clap",
"color-eyre",
@@ -893,7 +893,7 @@ dependencies = [
[[package]]
name = "komorebic-no-console"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
[[package]]
name = "kqueue"
@@ -923,9 +923,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.153"
version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
[[package]]
name = "libredox"
@@ -945,9 +945,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "lock_api"
version = "0.4.11"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
@@ -1011,7 +1011,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -1205,7 +1205,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -1263,9 +1263,9 @@ checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
[[package]]
name = "parking_lot"
version = "0.12.1"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -1273,18 +1273,18 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.9"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"backtrace",
"cfg-if 1.0.0",
"libc",
"petgraph",
"redox_syscall",
"redox_syscall 0.5.1",
"smallvec",
"thread-id",
"windows-targets 0.48.5",
"windows-targets 0.52.5",
]
[[package]]
@@ -1326,7 +1326,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -1391,9 +1391,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.80"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
@@ -1466,6 +1466,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
dependencies = [
"bitflags 2.5.0",
]
[[package]]
name = "redox_users"
version = "0.4.5"
@@ -1523,9 +1532,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "reqwest"
version = "0.12.3"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19"
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [
"base64",
"bytes",
@@ -1572,9 +1581,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.38.32"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.5.0",
"errno",
@@ -1595,9 +1604,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
[[package]]
name = "rustversion"
@@ -1631,9 +1640,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.16"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
checksum = "0afe01b987fac84253ce8acd5c05af9941975e4dee5b4f2d826b6947be8ec2c7"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -1643,14 +1652,14 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.16"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
checksum = "d253e72f060451e9e5615a1686f3cb4ff87c4e70504c79bdab8fb3b010cd4e97"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 1.0.109",
"syn 2.0.60",
]
[[package]]
@@ -1684,40 +1693,40 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.197"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
name = "serde_derive_internals"
version = "0.26.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.60",
]
[[package]]
name = "serde_json"
version = "1.0.115"
version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
dependencies = [
"itoa",
"ryu",
@@ -1792,9 +1801,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "socket2"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -1825,7 +1834,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -1862,9 +1871,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.59"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [
"proc-macro2",
"quote",
@@ -1879,9 +1888,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sysinfo"
version = "0.30.10"
version = "0.30.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d7c217777061d5a2d652aea771fb9ba98b6dade657204b08c4b9604d11555b"
checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
@@ -1948,22 +1957,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.58"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.58"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -2132,7 +2141,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -2230,9 +2239,9 @@ dependencies = [
[[package]]
name = "unicode-width"
version = "0.1.11"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
[[package]]
name = "unsafe-libyaml"
@@ -2321,7 +2330,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
"wasm-bindgen-shared",
]
@@ -2355,7 +2364,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -2412,11 +2421,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
dependencies = [
"winapi",
"windows-sys 0.52.0",
]
[[package]]
@@ -2474,7 +2483,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]
@@ -2485,7 +2494,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
"syn 2.0.60",
]
[[package]]

View File

@@ -18,6 +18,8 @@ dirs = "5"
color-eyre = "0.6"
serde_json = { package = "serde_json_lenient", version = "0.1" }
sysinfo = "0.30"
serde = { version = "1", features = ["derive"] }
uds_windows = "1"
[workspace.dependencies.windows]
version = "0.54"

View File

@@ -84,6 +84,19 @@ using `scoop`, `winget` or building from source.
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/H9-_c1egQ4g/hqdefault.jpg)](https://www.youtube.com/watch?v=H9-_c1egQ4g)
# Comparison With Fancy Zones
Community member [Olge](https://www.youtube.com/@polle5555) has created an
excellent video which compares the default window management features of
Windows 11, Fancy Zones and komorebi.
If you are not familiar with tiling window managers or if you are looking at
komorebi and wondering "how is this different from Fancy Zones? 🤔", this short
video will answer the majority of your questions.
[![Watch the comparison video](https://img.youtube.com/vi/0LCbS_gm0RA/hqdefault.jpg)](https://www.youtube.com/watch?v=0LCbS_gm0RA)
# Demonstrations
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
@@ -338,7 +351,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.24"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.25"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
Arguments:
<DEFAULT_LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

12
docs/cli/global-state.md Normal file
View File

@@ -0,0 +1,12 @@
# global-state
```
Show a JSON representation of the current global state
Usage: komorebic.exe global-state
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,19 @@
# move-to-monitor-workspace
```
Move the focused window to the specified monitor workspace
Usage: komorebic.exe move-to-monitor-workspace <TARGET_MONITOR> <TARGET_WORKSPACE>
Arguments:
<TARGET_MONITOR>
Target monitor index (zero-indexed)
<TARGET_WORKSPACE>
Workspace index on the target monitor (zero-indexed)
Options:
-h, --help
Print help
```

View File

@@ -13,7 +13,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule
<LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -10,7 +10,7 @@ Arguments:
Target workspace name
<VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -16,7 +16,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule
<LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -13,7 +13,7 @@ Arguments:
Workspace index on the specified monitor (zero-indexed)
<VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -2,7 +2,7 @@
❗️**NOTE**: A significant number of force-manage window rules for the most
common applications are [already generated for
you](https://github.com/LGUG2Z/komorebi/#generating-common-application-specific-configurations)
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
In some rare cases, a window may not automatically be registered to be managed
by `komorebi`. You can add rules to enforce this behaviour in the

View File

@@ -2,7 +2,7 @@
❗️**NOTE**: A significant number of ignored window rules for the most common
applications are [already generated for
you](https://github.com/LGUG2Z/komorebi/#generating-common-application-specific-configurations)
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
Sometimes you will want a specific application to never be tiled, and instead
float all the time. You can add rules to enforce this behaviour in the

View File

@@ -101,6 +101,16 @@ monocle.
+-------+-----+
```
#### RightMainVerticalStack
```
+-----+-------+
| | |
+-----+ |
| | |
+-----+-------+
```
#### Horizontal Stack
```
@@ -122,6 +132,7 @@ monocle.
```
#### Rows
If you have a vertical monitor, I recommend using this layout.
```
@@ -133,6 +144,7 @@ If you have a vertical monitor, I recommend using this layout.
```
#### Ultrawide Vertical Stack
If you have an ultrawide monitor, I recommend using this layout.
```
@@ -146,6 +158,7 @@ If you have an ultrawide monitor, I recommend using this layout.
```
### Grid
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
```
@@ -183,7 +196,8 @@ which shell you use in your terminal.
* `powershell` - set this if you are using the version of PowerShell that comes
installed with Windows 10+ (the executable file for this is `powershell.exe`)
* `pwsh` - set this if you are using PowerShell 7+, which you have installed yourself either through the Windows Store or WinGet (the executable file for this is `pwsh.exe`)
* `pwsh` - set this if you are using PowerShell 7+, which you have installed yourself either through the Windows Store
or WinGet (the executable file for this is `pwsh.exe`)
* `cmd` - set this if you don't want to use PowerShell at all and instead you
want to call commands through the shell used by the old-school Command

View File

@@ -1,11 +1,11 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.24/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.25/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
"default_workspace_padding": 20,
"default_container_padding": 20,
"border_padding": 8,
"border_width": 8,
"border_offset": -1,
"active_window_border": false,
"active_window_border_colours": {
@@ -13,6 +13,16 @@
"stack": "#00a542",
"monocle": "#ff3399"
},
"stackbar": {
"height": 40,
"mode": "Never",
"tabs": {
"width": 300,
"focused_text": "#00a542",
"unfocused_text": "#b3b3b3",
"background": "#141414"
}
},
"monitors": [
{
"workspaces": [
@@ -35,6 +45,14 @@
{
"name": "V",
"layout": "Rows"
},
{
"name": "VI",
"layout": "Grid"
},
{
"name": "VII",
"layout": "RightMainVerticalStack"
}
]
}

124
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,124 @@
# Troubleshooting
## AutoHotKey executable not found
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
not installed AHK using `scoop`, you'll probably receive an error:
```text
Error: could not find autohotkey, please make sure it is installed before using the --ahk flag
```
Depending on how AHK is installed the executable on your system may have a
different name. In order to account for this, you may set the `KOMOREBI_AHK_EXE`
environment variable in your
[PowerShell profile](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.4)
to match the name of the executable as it is found on your system.
After setting `KOMOREBI_AHK_EXE` make sure to either reload your PowerShell
profile or open a new terminal tab.
## Komorebi is unresponsive when the display wakes from sleep
This can happen in rare cases when your monitor state is not preserved after it
wakes from sleep.
### Problem
Your hotkeys in _whkd_ work, but it feels as if _komorebi_ knows nothing about
the previous state (you can't control previous windows, although newly launched ones
can be manipulated as normal).
### Solution
Some monitors, such as the Samsung G8/G9 (LED, Neo, OLED) have an _adaptive
sync_ or _variable refresh rate_ setting within the actual monitor OSD that can
disrupt how the device is persisted in the _komorebi_ state following suspension.
To fix this, please try to disable _Adaptive Sync_ or any other _VRR_ branded
alias by referring to the manufacturer's documentation.
!!! warning
Disabling VRR within Windows (e.g. _Nvidia Control Panel_) may work and can indeed
change the configuration you see within your monitor's OSD, but some monitors
will re-enable the setting regardless following suspension.
### Reproducing
Ensure _komorebi_ is in an operational state by executing `komorebic start` as
normal.
If _komorebi_ is already unresponsive, then please restart _komorebi_ first by
running `komorebic stop` and `komorebic start`.
1. **`komorebic state`**
```json
{
"monitors": {
"elements": [
{
"id": 65537,
"name": "DISPLAY1",
"device": "SAM71AA",
"device_id": "SAM71AA-5&a1a3e88&0&UID24834",
"size": {
"left": 0,
"top": 0,
"right": 5120,
"bottom": 1440
}
}
]
}
}
```
This appears to be fine -- _komorebi_ is aware of the device and associated
window handles.
2. **Let your display go to sleep.**
Simply turning the monitor off is not enough to reproduce the problem; you must
let Windows turn off the display itself.
To avoid waiting an eternity:
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
Settings_
_Turn off the display: 1 minute_
Allow a minute for the display to reset, then once it actually shuts off
allow for any additional time as prompted by your monitor for the cycle to
complete.
3. **Wake your display again** by pressing any key.
_komorebi_ should now be unresponsive.
4. **`komorebic state`**
Don't stop _komorebi_ just yet.
Since it's unresponsive, you can open another shell instead to execute the above command.
```json
{
"monitors": {
"elements": [
{
"id": 65537,
"name": "DISPLAY1",
"device": null,
"device_id": null
}
]
}
}
```
We can see the _komorebi_ state is no longer associated with the previous
device: `null`, suggesting an issue when the display resumes from a suspended
state.

View File

@@ -28,7 +28,6 @@ install:
just install-target komorebi
run:
just install-target komorebic
cargo +stable run --bin komorebi --locked
warn $RUST_LOG="warn":
@@ -44,7 +43,6 @@ trace $RUST_LOG="trace":
just run
deadlock $RUST_LOG="trace":
just install-komorebic
cargo +stable run --bin komorebi --locked --features deadlock_detection
docgen:

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -26,7 +26,7 @@ pub trait Arrangement {
}
impl Arrangement for DefaultLayout {
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
fn calculate(
&self,
area: &Rect,
@@ -44,8 +44,56 @@ impl Arrangement for DefaultLayout {
layout_flip,
calculate_resize_adjustments(resize_dimensions),
),
Self::Columns => columns(area, len),
Self::Rows => rows(area, len),
Self::Columns => {
let mut layouts = columns(area, len);
let adjustment = calculate_columns_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
columns_reverse(&mut layouts);
}
}
layouts
}
Self::Rows => {
let mut layouts = rows(area, len);
let adjustment = calculate_rows_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
rows_reverse(&mut layouts);
}
}
layouts
}
Self::VerticalStack => {
let mut layouts: Vec<Rect> = vec![];
@@ -54,16 +102,8 @@ impl Arrangement for DefaultLayout {
_ => area.right / 2,
};
let mut main_left = area.left;
let mut stack_left = area.left + primary_right;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
main_left = main_left + area.right - primary_right;
stack_left = area.left;
}
_ => {}
}
let main_left = area.left;
let stack_left = area.left + primary_right;
if len >= 1 {
layouts.push(Rect {
@@ -86,6 +126,113 @@ impl Arrangement for DefaultLayout {
}
}
let adjustment = calculate_vertical_stack_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.left = primary.left;
}
primary.left = rest[0].left + rest[0].right;
}
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
}
Self::RightMainVerticalStack => {
// Shamelessly borrowed from LeftWM: https://github.com/leftwm/leftwm/commit/f673851745295ae7584a102535566f559d96a941
let mut layouts: Vec<Rect> = vec![];
let primary_width = match len {
1 => area.right,
_ => area.right / 2,
};
let primary_left = match len {
1 => 0,
_ => area.right - primary_width,
};
if len >= 1 {
layouts.push(Rect {
left: area.left + primary_left,
top: area.top,
right: primary_width,
bottom: area.bottom,
});
if len > 1 {
layouts.append(&mut rows(
&Rect {
left: area.left,
top: area.top,
right: primary_left,
bottom: area.bottom,
},
len - 1,
));
}
}
let adjustment = calculate_right_vertical_stack_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
primary.left = rest[0].left;
for rect in rest.iter_mut() {
rect.left = primary.left + primary.right;
}
}
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
}
Self::HorizontalStack => {
@@ -96,16 +243,8 @@ impl Arrangement for DefaultLayout {
_ => area.bottom / 2,
};
let mut main_top = area.top;
let mut stack_top = area.top + bottom;
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) if len > 1 => {
main_top = main_top + area.bottom - bottom;
stack_top = area.top;
}
_ => {}
}
let main_top = area.top;
let stack_top = area.top + bottom;
if len >= 1 {
layouts.push(Rect {
@@ -128,9 +267,155 @@ impl Arrangement for DefaultLayout {
}
}
let adjustment = calculate_horizontal_stack_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.top = primary.top;
}
primary.top = rest[0].top + rest[0].bottom;
}
}
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
columns_reverse(&mut layouts[1..]);
}
}
layouts
}
Self::UltrawideVerticalStack => {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
1 => area.right,
_ => area.right / 2,
};
let secondary_right = match len {
1 => 0,
2 => area.right - primary_right,
_ => (area.right - primary_right) / 2,
};
let (primary_left, secondary_left, stack_left) = match len {
1 => (area.left, 0, 0),
2 => {
let primary = area.left + secondary_right;
let secondary = area.left;
(primary, secondary, 0)
}
_ => {
let primary = area.left + secondary_right;
let secondary = area.left;
let stack = area.left + primary_right + secondary_right;
(primary, secondary, stack)
}
};
if len >= 1 {
layouts.push(Rect {
left: primary_left,
top: area.top,
right: primary_right,
bottom: area.bottom,
});
if len >= 2 {
layouts.push(Rect {
left: secondary_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
});
if len > 2 {
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
},
len - 2,
));
}
}
}
let adjustment = calculate_ultrawide_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
match len {
2 => {
let (primary, secondary) = layouts.split_at_mut(1);
let primary = &mut primary[0];
let secondary = &mut secondary[0];
primary.left = secondary.left;
secondary.left = primary.left + primary.right;
}
3.. => {
let (primary, rest) = layouts.split_at_mut(1);
let (secondary, tertiary) = rest.split_at_mut(1);
let primary = &mut primary[0];
let secondary = &mut secondary[0];
for rect in tertiary.iter_mut() {
rect.left = secondary.left;
}
primary.left = tertiary[0].left + tertiary[0].right;
secondary.left = primary.left + primary.right;
}
_ => {}
}
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 4.. = len {
rows_reverse(&mut layouts[2..]);
}
}
layouts
}
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
@@ -369,6 +654,22 @@ fn rows(area: &Rect, len: usize) -> Vec<Rect> {
layouts
}
fn columns_reverse(columns: &mut [Rect]) {
let len = columns.len();
columns[len - 1].left = columns[0].left;
for i in (0..len - 1).rev() {
columns[i].left = columns[i + 1].left + columns[i + 1].right;
}
}
fn rows_reverse(rows: &mut [Rect]) {
let len = rows.len();
rows[len - 1].top = rows[0].top;
for i in (0..len - 1).rev() {
rows[i].top = rows[i + 1].top + rows[i + 1].bottom;
}
}
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
let mut resize_adjustments = resize_dimensions.to_vec();
@@ -562,6 +863,187 @@ fn recursive_fibonacci(
}
}
fn calculate_columns_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
0 | 1 => (),
_ => {
for (i, rect) in resize_dimensions.iter().enumerate() {
if let Some(rect) = rect {
if i != 0 {
resize_right(&mut result[i - 1], rect.left);
resize_left(&mut result[i], rect.left);
}
if i != len - 1 {
resize_right(&mut result[i], rect.right);
resize_left(&mut result[i + 1], rect.right);
}
}
}
}
};
result
}
fn calculate_rows_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
0 | 1 => (),
_ => {
for (i, rect) in resize_dimensions.iter().enumerate() {
if let Some(rect) = rect {
if i != 0 {
resize_bottom(&mut result[i - 1], rect.top);
resize_top(&mut result[i], rect.top);
}
if i != len - 1 {
resize_bottom(&mut result[i], rect.bottom);
resize_top(&mut result[i + 1], rect.bottom);
}
}
}
}
};
result
}
fn calculate_vertical_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
// One container can't be resized
0 | 1 => (),
_ => {
let (master, stack) = result.split_at_mut(1);
let primary = &mut master[0];
if let Some(resize) = resize_dimensions[0] {
resize_right(primary, resize.right);
for s in &mut *stack {
resize_left(s, resize.right);
}
}
// Handle stack on the right
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
if let Some(rect) = rect {
resize_right(primary, rect.left);
stack
.iter_mut()
.for_each(|vertical_element| resize_left(vertical_element, rect.left));
// Containers in stack except first can be resized up displacing container
// above them
if i != 0 {
resize_bottom(&mut stack[i - 1], rect.top);
resize_top(&mut stack[i], rect.top);
}
// Containers in stack except last can be resized down displacing container
// below them
if i != stack.len() - 1 {
resize_bottom(&mut stack[i], rect.bottom);
resize_top(&mut stack[i + 1], rect.bottom);
}
}
}
}
};
result
}
fn calculate_right_vertical_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
// One container can't be resized
0 | 1 => (),
_ => {
let (master, stack) = result.split_at_mut(1);
let primary = &mut master[0];
if let Some(resize) = resize_dimensions[0] {
resize_left(primary, resize.left);
for s in &mut *stack {
resize_right(s, resize.left);
}
}
// Handle stack on the left
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
if let Some(rect) = rect {
resize_left(primary, rect.right);
stack
.iter_mut()
.for_each(|vertical_element| resize_right(vertical_element, rect.right));
// Containers in stack except first can be resized up displacing container
// above them
if i != 0 {
resize_bottom(&mut stack[i - 1], rect.top);
resize_top(&mut stack[i], rect.top);
}
// Containers in stack except last can be resized down displacing container
// below them
if i != stack.len() - 1 {
resize_bottom(&mut stack[i], rect.bottom);
resize_top(&mut stack[i + 1], rect.bottom);
}
}
}
}
};
result
}
fn calculate_horizontal_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
0 | 1 => (),
_ => {
let (primary, rest) = result.split_at_mut(1);
let primary = &mut primary[0];
if let Some(resize_primary) = resize_dimensions[0] {
resize_bottom(primary, resize_primary.bottom);
for horizontal_element in &mut *rest {
resize_top(horizontal_element, resize_primary.bottom);
}
}
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
if let Some(rect) = rect {
resize_bottom(primary, rect.top);
rest.iter_mut()
.for_each(|vertical_element| resize_top(vertical_element, rect.top));
if i != 0 {
resize_right(&mut rest[i - 1], rect.left);
resize_left(&mut rest[i], rect.left);
}
if i != rest.len() - 1 {
resize_right(&mut rest[i], rect.right);
resize_left(&mut rest[i + 1], rect.right);
}
}
}
}
};
result
}
fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
@@ -652,99 +1134,3 @@ fn resize_top(rect: &mut Rect, resize: i32) {
fn resize_bottom(rect: &mut Rect, resize: i32) {
rect.bottom += resize / 2;
}
fn ultrawide(
area: &Rect,
len: usize,
layout_flip: Option<Axis>,
resize_dimensions: &[Option<Rect>],
) -> Vec<Rect> {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
1 => area.right,
_ => area.right / 2,
};
let secondary_right = match len {
1 => 0,
2 => area.right - primary_right,
_ => (area.right - primary_right) / 2,
};
let (primary_left, secondary_left, stack_left) = match len {
1 => (area.left, 0, 0),
2 => {
let mut primary = area.left + secondary_right;
let mut secondary = area.left;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
primary = area.left;
secondary = area.left + primary_right;
}
_ => {}
}
(primary, secondary, 0)
}
_ => {
let primary = area.left + secondary_right;
let mut secondary = area.left;
let mut stack = area.left + primary_right + secondary_right;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
secondary = area.left + primary_right + secondary_right;
stack = area.left;
}
_ => {}
}
(primary, secondary, stack)
}
};
if len >= 1 {
layouts.push(Rect {
left: primary_left,
top: area.top,
right: primary_right,
bottom: area.bottom,
});
if len >= 2 {
layouts.push(Rect {
left: secondary_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
});
if len > 2 {
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
},
len - 2,
));
}
}
}
let adjustment = calculate_ultrawide_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
layouts
}

View File

@@ -30,6 +30,7 @@ pub enum DefaultLayout {
HorizontalStack,
UltrawideVerticalStack,
Grid,
RightMainVerticalStack,
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
}
@@ -44,7 +45,16 @@ impl DefaultLayout {
sizing: Sizing,
delta: i32,
) -> Option<Rect> {
if !matches!(self, Self::BSP) && !matches!(self, Self::UltrawideVerticalStack) {
if !matches!(
self,
Self::BSP
| Self::Columns
| Self::Rows
| Self::VerticalStack
| Self::RightMainVerticalStack
| Self::HorizontalStack
| Self::UltrawideVerticalStack
) {
return None;
};
@@ -146,7 +156,8 @@ impl DefaultLayout {
Self::VerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::Grid,
Self::Grid => Self::BSP,
Self::Grid => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::BSP,
}
}
@@ -159,7 +170,8 @@ impl DefaultLayout {
Self::VerticalStack => Self::Rows,
Self::Rows => Self::Columns,
Self::Columns => Self::Grid,
Self::Grid => Self::BSP,
Self::Grid => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::BSP,
}
}
}

View File

@@ -95,7 +95,7 @@ impl Direction for DefaultLayout {
Self::BSP => count > 2 && idx != 0 && idx != 1,
Self::Columns => false,
Self::Rows | Self::HorizontalStack => idx != 0,
Self::VerticalStack => idx != 0 && idx != 1,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx > 2,
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
@@ -103,7 +103,7 @@ impl Direction for DefaultLayout {
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
Self::Columns => false,
Self::Rows => idx != count - 1,
Self::VerticalStack => idx != 0 && idx != count - 1,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
Self::HorizontalStack => idx == 0,
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
Self::Grid => !is_grid_edge(op_direction, idx, count),
@@ -111,6 +111,7 @@ impl Direction for DefaultLayout {
OperationDirection::Left => match self {
Self::BSP => count > 1 && idx != 0,
Self::Columns | Self::VerticalStack => idx != 0,
Self::RightMainVerticalStack => idx == 0,
Self::Rows => false,
Self::HorizontalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => count > 1 && idx != 1,
@@ -121,6 +122,7 @@ impl Direction for DefaultLayout {
Self::Columns => idx != count - 1,
Self::Rows => false,
Self::VerticalStack => idx == 0,
Self::RightMainVerticalStack => idx != 0,
Self::HorizontalStack => idx != 0 && idx != count - 1,
Self::UltrawideVerticalStack => match count {
0 | 1 => false,
@@ -147,7 +149,10 @@ impl Direction for DefaultLayout {
}
}
Self::Columns => unreachable!(),
Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx - 1,
Self::Rows
| Self::VerticalStack
| Self::UltrawideVerticalStack
| Self::RightMainVerticalStack => idx - 1,
Self::HorizontalStack => 0,
Self::Grid => grid_neighbor(op_direction, idx, count),
}
@@ -160,7 +165,11 @@ impl Direction for DefaultLayout {
count: Option<usize>,
) -> usize {
match self {
Self::BSP | Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx + 1,
Self::BSP
| Self::Rows
| Self::VerticalStack
| Self::UltrawideVerticalStack
| Self::RightMainVerticalStack => idx + 1,
Self::Columns => unreachable!(),
Self::HorizontalStack => 1,
Self::Grid => grid_neighbor(op_direction, idx, count),
@@ -184,6 +193,7 @@ impl Direction for DefaultLayout {
Self::Columns | Self::HorizontalStack => idx - 1,
Self::Rows => unreachable!(),
Self::VerticalStack => 0,
Self::RightMainVerticalStack => 1,
Self::UltrawideVerticalStack => match idx {
0 => 1,
1 => unreachable!(),
@@ -203,6 +213,7 @@ impl Direction for DefaultLayout {
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
Self::Rows => unreachable!(),
Self::VerticalStack => 1,
Self::RightMainVerticalStack => 0,
Self::UltrawideVerticalStack => match idx {
1 => 0,
0 => 2,

View File

@@ -218,6 +218,7 @@ pub enum WindowKind {
Single,
Stack,
Monocle,
Unfocused,
}
#[derive(
@@ -282,6 +283,8 @@ pub enum MoveBehaviour {
Swap,
/// Insert the window container into the focused workspace on the adjacent monitor
Insert,
/// Do nothing if trying to move a window container in the direction of an adjacent monitor
NoOp,
}
#[derive(

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]

View File

@@ -1,108 +0,0 @@
use std::sync::atomic::Ordering;
use color_eyre::Result;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::FindWindowW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use crate::window::Window;
use crate::windows_callbacks;
use crate::WindowsApi;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_RECT;
use crate::BORDER_WIDTH;
use crate::TRANSPARENCY_COLOUR;
#[derive(Debug, Clone, Copy)]
pub struct Border {
pub(crate) hwnd: isize,
}
impl From<isize> for Border {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Border {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
pub fn create(name: &str) -> Result<()> {
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
let instance = WindowsApi::module_handle_w()?;
let class_name = PCWSTR(name.as_ptr());
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
let window_class = WNDCLASSW {
hInstance: instance.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(windows_callbacks::border_window),
hbrBackground: brush,
..Default::default()
};
let _atom = WindowsApi::register_class_w(&window_class)?;
let name_cl = name.clone();
std::thread::spawn(move || -> Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name_cl.as_ptr()), instance)?;
let border = Self::from(hwnd);
let mut message = MSG::default();
unsafe {
while GetMessageW(&mut message, border.hwnd(), 0, 0).into() {
DispatchMessageW(&message);
}
}
Ok(())
});
let mut hwnd = HWND(0);
while hwnd == HWND(0) {
hwnd = unsafe { FindWindowW(PCWSTR(name.as_ptr()), PCWSTR::null()) };
}
BORDER_HWND.store(hwnd.0, Ordering::SeqCst);
Ok(())
}
pub fn hide(self) -> Result<()> {
if self.hwnd == 0 {
Ok(())
} else {
WindowsApi::hide_border_window(self.hwnd())
}
}
pub fn set_position(self, window: Window, activate: bool) -> Result<()> {
if self.hwnd == 0 {
Ok(())
} else {
if !WindowsApi::is_window(self.hwnd()) {
Self::create("komorebi-border-window")?;
}
let mut rect = WindowsApi::window_rect(window.hwnd())?;
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
rect.add_margin(border_width);
*BORDER_RECT.lock() = rect;
WindowsApi::position_border_window(self.hwnd(), &rect, activate)
}
}
}

View File

@@ -0,0 +1,224 @@
use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUSED;
use crate::border_manager::FOCUS_STATE;
use crate::border_manager::MONOCLE;
use crate::border_manager::RECT_STATE;
use crate::border_manager::STACK;
use crate::border_manager::STYLE;
use crate::border_manager::UNFOCUSED;
use crate::border_manager::Z_ORDER;
use crate::WindowsApi;
use crate::WINDOWS_11;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::Rect;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use windows::core::PCWSTR;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
if class.starts_with("komoborder") {
hwnds.push(hwnd.0);
}
}
true.into()
}
#[derive(Debug)]
pub struct Border {
pub hwnd: isize,
}
impl From<isize> for Border {
fn from(value: isize) -> Self {
Self { hwnd: value }
}
}
impl Border {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let window_class = WNDCLASSW {
hInstance: h_module.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), h_module)?;
hwnd_sender.send(hwnd)?;
let mut message = MSG::default();
unsafe {
while GetMessageW(&mut message, HWND(hwnd), 0, 0).into() {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?,
})
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::close_window(self.hwnd())
}
pub fn update(&self, rect: &Rect) -> color_eyre::Result<()> {
// Make adjustments to the border
let mut rect = *rect;
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
// Store the border rect so that it can be used by the callback
{
let mut rects = RECT_STATE.lock();
rects.insert(self.hwnd, rect);
}
// Update the position of the border if required
if !WindowsApi::window_rect(self.hwnd())?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd(), &rect, HWND((*Z_ORDER.lock()).into()))?;
}
// Invalidate the rect to trigger the callback to update colours etc.
self.invalidate();
Ok(())
}
pub fn invalidate(&self) {
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
}
pub extern "system" fn callback(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_PAINT => {
let rects = RECT_STATE.lock();
// With the rect that we stored in Self::update
if let Some(rect) = rects.get(&window.0).copied() {
// Grab the focus kind for this border
let focus_kind = {
FOCUS_STATE
.lock()
.get(&window.0)
.copied()
.unwrap_or(WindowKind::Unfocused)
};
// Set up the brush to draw the border
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
let hpen = CreatePen(
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(match focus_kind {
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
}),
);
let hbrush = WindowsApi::create_solid_brush(0);
// Draw the border
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
// TODO(raggi): this is approximately the correct curvature for
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
// often the bottom right has a different shape. Furthermore if
// the window was made with DWMWCP_ROUNDSMALL then this is the
// wrong size. In the future we should read the DWM properties
// of windows and attempt to match appropriately.
match *STYLE.lock() {
ActiveWindowBorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
ActiveWindowBorderStyle::Rounded => {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
}
ActiveWindowBorderStyle::Square => {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
EndPaint(window, &ps);
ValidateRect(window, None);
}
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
}

View File

@@ -0,0 +1,362 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::ActiveWindowBorderStyle;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
use windows::Win32::Foundation::HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::Colour;
use crate::Rect;
use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
use border::Border;
use komorebi_core::WindowKind;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
lazy_static! {
pub static ref Z_ORDER: Arc<Mutex<ZOrder>> = Arc::new(Mutex::new(ZOrder::Bottom));
pub static ref STYLE: Arc<Mutex<ActiveWindowBorderStyle>> =
Arc::new(Mutex::new(ActiveWindowBorderStyle::System));
pub static ref FOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
pub static ref UNFOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(128, 128, 128))));
pub static ref MONOCLE: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
}
lazy_static! {
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref RECT_STATE: Mutex<HashMap<isize, Rect>> = Mutex::new(HashMap::new());
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
}
pub struct Notification;
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(crossbeam_channel::unbounded)
}
pub fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
pub fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn destroy_all_borders() -> color_eyre::Result<()> {
let mut borders = BORDER_STATE.lock();
tracing::info!(
"purging known borders: {:?}",
borders.iter().map(|b| b.1.hwnd).collect::<Vec<_>>()
);
for (_, border) in borders.iter() {
border.destroy()?;
}
borders.clear();
RECT_STATE.lock().clear();
BORDERS_MONITORS.lock().clear();
FOCUS_STATE.lock().clear();
let mut remaining_hwnds = vec![];
WindowsApi::enum_windows(
Some(border_hwnds),
&mut remaining_hwnds as *mut Vec<isize> as isize,
)?;
if !remaining_hwnds.is_empty() {
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
for hwnd in remaining_hwnds {
Border::from(hwnd).destroy()?;
}
}
Ok(())
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
let mut instant: Option<Instant> = None;
event_tx().send(Notification)?;
'receiver: for _ in receiver {
if let Some(instant) = instant {
if instant.elapsed().lt(&Duration::from_millis(50)) {
continue 'receiver;
}
}
instant = Some(Instant::now());
let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();
// Check the wm state every time we receive a notification
let state = wm.lock();
// If borders are disabled
if !BORDER_ENABLED.load_consume()
// Or if the wm is paused
|| state.is_paused
// Or if we are handling an alt-tab across workspaces
|| ALT_TAB_HWND.load().is_some()
{
// Destroy the borders we know about
for (_, border) in borders.iter() {
border.destroy()?;
}
borders.clear();
continue 'receiver;
}
let focused_monitor_idx = state.focused_monitor_idx();
for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Workspaces with tiling disabled don't have borders
if !ws.tile() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'receiver;
}
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
let border = match borders.entry(monocle.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(monocle.id()) {
entry.insert(border)
} else {
continue 'receiver;
}
}
};
borders_monitors.insert(monocle.id().clone(), monitor_idx);
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(border.hwnd, WindowKind::Monocle);
}
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd(),
)?;
border.update(&rect)?;
continue 'receiver;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
if is_maximized {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'receiver;
}
// Destroy any borders not associated with the focused workspace
let container_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
{
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
for (idx, c) in ws.containers().iter().enumerate() {
// Update border when moving or resizing with mouse
if state.pending_move_op.is_some() && idx == ws.focused_container_idx() {
let restore_z_order = *Z_ORDER.lock();
*Z_ORDER.lock() = ZOrder::TopMost;
let mut rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
while WindowsApi::lbutton_is_pressed() {
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'receiver;
}
}
};
let new_rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect)?;
}
}
*Z_ORDER.lock() = restore_z_order;
continue 'receiver;
}
// Get the border entry for this container from the map or create one
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'receiver;
}
}
};
borders_monitors.insert(c.id().clone(), monitor_idx);
// Update the focused state for all containers on this workspace
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(
border.hwnd,
if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
WindowKind::Stack
} else {
WindowKind::Single
},
);
}
let rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd(),
)?;
border.update(&rect)?;
}
}
}
}
Ok(())
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
pub enum ZOrder {
Top,
NoTopMost,
Bottom,
TopMost,
}
impl From<ZOrder> for isize {
fn from(val: ZOrder) -> Self {
match val {
ZOrder::Top => 0,
ZOrder::NoTopMost => -2,
ZOrder::Bottom => 1,
ZOrder::TopMost => -1,
}
}
}

View File

@@ -65,7 +65,7 @@ pub struct Rgb {
}
impl Rgb {
pub fn new(r: u32, g: u32, b: u32) -> Self {
pub const fn new(r: u32, g: u32, b: u32) -> Self {
Self { r, g, b }
}
}

View File

@@ -18,6 +18,7 @@ pub struct Container {
#[getset(get = "pub")]
id: String,
windows: Ring<Window>,
#[serde(skip)]
#[getset(get = "pub", get_mut = "pub")]
stackbar: Option<Stackbar>,
}
@@ -124,7 +125,10 @@ impl Container {
let window = self.windows_mut().remove(idx);
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack) && self.windows().len() <= 1 {
self.stackbar = None;
if let Some(stackbar) = &self.stackbar {
let _ = WindowsApi::close_window(stackbar.hwnd());
self.stackbar = None;
}
}
if idx != 0 {
@@ -159,27 +163,39 @@ impl Container {
}
pub fn set_stackbar_mode(&mut self, mode: StackbarMode) {
self.stackbar = match mode {
StackbarMode::Always => Stackbar::create().ok(),
StackbarMode::Never => None,
StackbarMode::OnStack => {
if self.windows().len() > 1 && self.stackbar().is_none() {
Stackbar::create().ok()
} else {
None
match mode {
StackbarMode::Always => {
if self.stackbar.is_none() {
self.stackbar = Stackbar::create().ok();
}
}
};
}
StackbarMode::Never => {
if let Some(stackbar) = &self.stackbar {
let _ = WindowsApi::close_window(stackbar.hwnd());
}
pub fn renew_stackbar(&mut self) {
match &self.stackbar {
None => {}
Some(stackbar) => {
if !WindowsApi::is_window(stackbar.hwnd()) {
self.stackbar = Stackbar::create().ok()
self.stackbar = None
}
StackbarMode::OnStack => {
if self.windows().len() > 1 && self.stackbar().is_none() {
self.stackbar = Stackbar::create().ok();
}
if let Some(stackbar) = &self.stackbar {
if self.windows().len() == 1 {
let _ = WindowsApi::close_window(stackbar.hwnd());
self.stackbar = None;
}
}
}
}
}
pub fn renew_stackbar(&mut self) {
if let Some(stackbar) = &self.stackbar {
if !WindowsApi::is_window(stackbar.hwnd()) {
self.stackbar = Stackbar::create().ok()
}
}
}
}

View File

@@ -14,7 +14,6 @@ use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use crate::windows_callbacks;
use crate::WindowsApi;
use crate::HIDDEN_HWND;
use crate::TRANSPARENCY_COLOUR;
#[derive(Debug, Clone, Copy)]
pub struct Hidden {
@@ -36,7 +35,7 @@ impl Hidden {
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
let instance = WindowsApi::module_handle_w()?;
let class_name = PCWSTR(name.as_ptr());
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
let brush = WindowsApi::create_solid_brush(0);
let window_class = WNDCLASSW {
hInstance: instance.into(),
lpszClassName: class_name,

View File

@@ -1,4 +1,4 @@
pub mod border;
pub mod border_manager;
pub mod com;
#[macro_use]
pub mod ring;
@@ -21,6 +21,7 @@ pub mod windows_callbacks;
pub mod winevent;
pub mod winevent_listener;
pub mod workspace;
pub mod workspace_reconciliator;
use lazy_static::lazy_static;
use std::collections::HashMap;
@@ -53,7 +54,6 @@ use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ActiveWindowBorderStyle;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
use komorebi_core::Rect;
@@ -197,13 +197,6 @@ lazy_static! {
)
};
static ref ACTIVE_WINDOW_BORDER_STYLE: Arc<Mutex<ActiveWindowBorderStyle>> =
Arc::new(Mutex::new(ActiveWindowBorderStyle::System));
static ref BORDER_RECT: Arc<Mutex<Rect>> =
Arc::new(Mutex::new(Rect::default()));
// Use app-specific titlebar removal options where possible
// eg. Windows Terminal, IntelliJ IDEA, Firefox
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
@@ -220,18 +213,7 @@ pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false);
pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0);
pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false);
pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
pub const TRANSPARENCY_COLOUR: u32 = 0;
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);

View File

@@ -23,6 +23,7 @@ use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use komorebi::border_manager;
use komorebi::hidden::Hidden;
use komorebi::load_configuration;
use komorebi::process_command::listen_for_commands;
@@ -33,6 +34,7 @@ use komorebi::static_config::StaticConfig;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
use komorebi::winevent_listener;
use komorebi::workspace_reconciliator;
use komorebi::CUSTOM_FFM;
use komorebi::DATA_DIR;
use komorebi::HOME_DIR;
@@ -253,6 +255,9 @@ fn main() -> Result<()> {
listen_for_movements(wm.clone());
}
border_manager::listen_for_notifications(wm.clone());
workspace_reconciliator::listen_for_notifications(wm.clone());
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
ctrlc_sender

View File

@@ -36,6 +36,10 @@ pub struct Monitor {
work_area_size: Rect,
#[getset(get_copy = "pub", set = "pub")]
work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
window_based_work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
window_based_work_area_offset_limit: isize,
workspaces: Ring<Workspace>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
@@ -58,6 +62,8 @@ pub fn new(id: isize, size: Rect, work_area_size: Rect, name: String) -> Monitor
size,
work_area_size,
work_area_offset: None,
window_based_work_area_offset: None,
window_based_work_area_offset_limit: 1,
workspaces,
last_focused_workspace: None,
workspace_names: HashMap::default(),
@@ -194,6 +200,11 @@ impl Monitor {
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
let work_area = *self.work_area_size();
let window_based_work_area_offset = (
self.window_based_work_area_offset_limit(),
self.window_based_work_area_offset(),
);
let offset = if self.work_area_offset().is_some() {
self.work_area_offset()
} else {
@@ -202,7 +213,7 @@ impl Monitor {
self.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?
.update(&work_area, offset)?;
.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
}

View File

@@ -13,7 +13,6 @@ use std::sync::Arc;
use std::time::Duration;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use miow::pipe::connect;
use net2::TcpStreamExt;
@@ -39,7 +38,8 @@ use komorebi_core::StateQuery;
use komorebi_core::WindowContainerBehaviour;
use komorebi_core::WindowKind;
use crate::border::Border;
use crate::border_manager;
use crate::border_manager::STYLE;
use crate::colour::Rgb;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
@@ -52,16 +52,6 @@ use crate::windows_api::WindowsApi;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HIDDEN;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
@@ -88,23 +78,34 @@ use crate::WORKSPACE_RULES;
#[tracing::instrument]
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
let listener = wm
.lock()
.command_listener
.try_clone()
.expect("could not clone unix listener");
std::thread::spawn(move || loop {
let listener = wm
.lock()
.command_listener
.try_clone()
.expect("could not clone unix listener");
std::thread::spawn(move || {
tracing::info!("listening on komorebi.sock");
// client is unique for every komorebic command
for client in listener.incoming() {
match client {
Ok(stream) => match read_commands_uds(&wm, stream) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
}
},
Err(error) => {
tracing::error!("{}", error);
break;
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
}
}
}
@@ -175,21 +176,6 @@ impl WindowManager {
}
}
match message {
SocketMessage::CycleFocusMonitor(_)
| SocketMessage::CycleFocusWorkspace(_)
| SocketMessage::FocusMonitorNumber(_)
| SocketMessage::FocusMonitorWorkspaceNumber(_, _)
| SocketMessage::FocusWorkspaceNumber(_) => {
if self.focused_workspace()?.visible_windows().is_empty() {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
}
_ => {}
};
match message {
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
if let Some(monitor) = self.focused_monitor_mut() {
@@ -517,7 +503,10 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::Retile => self.retile_all(false)?,
SocketMessage::Retile => {
border_manager::destroy_all_borders()?;
self.retile_all(false)?
}
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
@@ -640,10 +629,6 @@ impl WindowManager {
);
self.focus_workspace(workspace_idx)?;
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::FocusLastWorkspace => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
@@ -667,10 +652,6 @@ impl WindowManager {
self.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?
.set_last_focused_workspace(Option::from(idx));
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
@@ -680,11 +661,9 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
}
self.focus_workspace(workspace_idx)?;
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
if self.focused_workspace_idx().unwrap_or_default() != workspace_idx {
self.focus_workspace(workspace_idx)?;
}
}
SocketMessage::FocusWorkspaceNumbers(workspace_idx) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
@@ -704,14 +683,17 @@ impl WindowManager {
}
self.focus_workspace(workspace_idx)?;
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
if focused_pair != (monitor_idx, workspace_idx) {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
}
SocketMessage::FocusNamedWorkspace(ref name) => {
if let Some((monitor_idx, workspace_idx)) =
@@ -720,10 +702,6 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
self.focus_workspace(workspace_idx)?;
}
if BORDER_ENABLED.load(Ordering::SeqCst) {
self.show_border()?;
};
}
SocketMessage::Stop => {
tracing::info!(
@@ -1229,6 +1207,7 @@ impl WindowManager {
MoveBehaviour::Insert => {
self.cross_monitor_move_behaviour = MoveBehaviour::Swap;
}
_ => {}
}
}
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {
@@ -1238,47 +1217,31 @@ impl WindowManager {
self.unmanaged_window_operation_behaviour = behaviour;
}
SocketMessage::ActiveWindowBorder(enable) => {
if enable {
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
Border::create("komorebi-border-window")?;
}
BORDER_ENABLED.store(true, Ordering::SeqCst);
self.show_border()?;
} else {
BORDER_ENABLED.store(false, Ordering::SeqCst);
self.hide_border()?;
}
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => {
match kind {
WindowKind::Single => {
BORDER_COLOUR_SINGLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Stack => {
BORDER_COLOUR_STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Monocle => {
BORDER_COLOUR_MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => match kind {
WindowKind::Single => {
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowsApi::invalidate_border_rect()?;
}
WindowKind::Stack => {
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Monocle => {
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Unfocused => {
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
},
SocketMessage::ActiveWindowBorderStyle(style) => {
let mut active_window_border_style = ACTIVE_WINDOW_BORDER_STYLE.lock();
let mut active_window_border_style = STYLE.lock();
*active_window_border_style = style;
WindowsApi::invalidate_border_rect()?;
}
SocketMessage::BorderWidth(width) => {
BORDER_WIDTH.store(width, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
}
SocketMessage::BorderOffset(offset) => {
BORDER_OFFSET.store(offset, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
}
SocketMessage::StackbarMode(mode) => {
let mut stackbar_mode = STACKBAR_MODE.lock();
@@ -1370,155 +1333,14 @@ impl WindowManager {
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
};
match message {
SocketMessage::ToggleMonocle => {
let current = BORDER_COLOUR_CURRENT.load(Ordering::SeqCst);
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
if monocle != 0 {
if current == monocle {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
} else {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
}
}
}
SocketMessage::StackWindow(_) => {
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
if stack != 0 {
BORDER_COLOUR_CURRENT
.store(BORDER_COLOUR_STACK.load(Ordering::SeqCst), Ordering::SeqCst);
}
}
SocketMessage::UnstackWindow => {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
}
_ => {}
}
match message {
SocketMessage::ChangeLayout(_)
| SocketMessage::CycleLayout(_)
| SocketMessage::ChangeLayoutCustom(_)
| SocketMessage::FlipLayout(_)
| SocketMessage::ManageFocusedWindow
| SocketMessage::MoveWorkspaceToMonitorNumber(_)
| SocketMessage::MoveContainerToMonitorNumber(_)
| SocketMessage::MoveContainerToWorkspaceNumber(_)
| SocketMessage::MoveContainerToMonitorWorkspaceNumber(_, _)
| SocketMessage::MoveContainerToNamedWorkspace(_)
| SocketMessage::ResizeWindowEdge(_, _)
| SocketMessage::ResizeWindowAxis(_, _)
| SocketMessage::ToggleFloat
| SocketMessage::ToggleMonocle
| SocketMessage::ToggleMaximize
| SocketMessage::Promote
| SocketMessage::PromoteFocus
| SocketMessage::StackWindow(_)
| SocketMessage::UnstackWindow
| SocketMessage::Retile
// Adding this one so that changes can be seen instantly after
// modifying the active window border offset
| SocketMessage::BorderOffset(_)
// Adding this one because sometimes EVENT_SYSTEM_FOREGROUND isn't
// getting sent on FocusWindow, meaning the border won't be set
// when processing events
| SocketMessage::FocusWindow(_)
| SocketMessage::InvisibleBorders(_)
| SocketMessage::WorkAreaOffset(_)
| SocketMessage::CycleMoveWindow(_)
| SocketMessage::MoveWindow(_)
| SocketMessage::CycleFocusMonitor(_)
| SocketMessage::CycleFocusWorkspace(_)
| SocketMessage::FocusMonitorNumber(_)
| SocketMessage::FocusMonitorWorkspaceNumber(_, _)
| SocketMessage::FocusWorkspaceNumber(_) => {
// The foreground window might be de-activating if we've just
// set it as a result of our own actions, so wait until the new
// one returns. This particularly happens when switching monitors.
//
// TODO(raggi): re-evaluate this branch. I checked the
// suggestion from the comment above, that we don't get
// EVENT_SYSTEM_FOREGROUND, but if I print out trace events I
// see that we do.
// XXX(raggi) We drop FocusChange events though for windows that
// we're not managing, so that's one of the ways that the border
// window gets stuck. We should stop overloading `should_manage`
// as an event filter, and separately filter events that we want
// to handle, and windows that we want to handle, as some events
// must be handled even if we're not managing the target window.
let mut attempts = 0;
let foreground = loop {
match WindowsApi::foreground_window() {
Ok(foreground) => break foreground,
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(10));
attempts+=1;
if attempts == 10 {
bail!("failed to get foreground window after 100ms")
}
}
};
};
let foreground_window = Window { hwnd: foreground };
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
if monocle != 0 && self.focused_workspace()?.monocle_container().is_some() {
BORDER_COLOUR_CURRENT.store(
monocle,
Ordering::SeqCst,
);
}
// it is not acceptable to fail here; we need to be able to send the event to
// subscribers
if self.focused_container().is_ok() {
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
if stack != 0 && self.focused_container()?.windows().len() > 1 {
BORDER_COLOUR_CURRENT
.store(stack, Ordering::SeqCst);
}
}
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.set_position(foreground_window, false)?;
}
SocketMessage::TogglePause => {
let is_paused = self.is_paused;
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
if is_paused {
border.hide()?;
} else {
let focused = self.focused_window()?;
border.set_position(*focused, true)?;
focused.focus(false)?;
}
}
SocketMessage::ToggleTiling | SocketMessage::WorkspaceTiling(..) => {
let tiling_enabled = *self.focused_workspace_mut()?.tile();
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
if tiling_enabled {
let focused = self.focused_window()?;
border.set_position(*focused, true)?;
focused.focus(false)?;
} else {
border.hide()?;
}
}
_ => {}
let notification = Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
};
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::event_tx().send(border_manager::Notification)?;
tracing::info!("processed");
Ok(())
}
@@ -1594,10 +1416,6 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
}
wm.process_command(message.clone(), &mut stream)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: wm.as_ref().into(),
})?)?;
}
Ok(())
@@ -1644,10 +1462,6 @@ pub fn read_commands_tcp(
}
wm.process_command(message.clone(), &mut *stream)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::Socket(message.clone()),
state: wm.as_ref().into(),
})?)?;
}
}
}

View File

@@ -1,6 +1,8 @@
use std::fs::OpenOptions;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
@@ -11,7 +13,9 @@ use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::WindowContainerBehaviour;
use crate::border::Border;
use crate::border_manager;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::window::should_act;
@@ -19,17 +23,11 @@ use crate::window::RuleDebug;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace_reconciliator;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HIDDEN;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DATA_DIR;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
@@ -37,20 +35,17 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
let receiver = wm.lock().incoming_events.clone();
std::thread::spawn(move || {
std::thread::spawn(move || loop {
tracing::info!("listening");
loop {
if let Ok(event) = receiver.recv() {
match wm.lock().process_event(event) {
Ok(()) => {}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
let receiver = wm.lock().incoming_events.clone();
for event in receiver {
match wm.lock().process_event(event) {
Ok(()) => {}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("{:?}", error)
} else {
tracing::error!("{}", error)
}
}
}
@@ -71,28 +66,6 @@ impl WindowManager {
let should_manage = event.window().should_manage(Some(event), &mut rule_debug)?;
// Hide or reposition the window based on whether the target is managed.
if BORDER_ENABLED.load(Ordering::SeqCst) {
if let WindowManagerEvent::FocusChange(_, window) = event {
let border_window = Border::from(BORDER_HWND.load(Ordering::SeqCst));
if should_manage {
border_window.set_position(window, true)?;
} else {
let mut stackbar = false;
if let Ok(class) = window.class() {
if class == "komorebi_stackbar" {
stackbar = true;
}
}
if !stackbar {
border_window.hide()?;
}
}
}
}
// All event handlers below this point should only be processed if the event is
// related to a window that should be managed by the WindowManager.
if !should_manage && !matches!(event, WindowManagerEvent::DisplayChange(_)) {
@@ -149,6 +122,11 @@ impl WindowManager {
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
@@ -162,7 +140,7 @@ impl WindowManager {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset)?;
workspace.update(&work_area, offset, window_based_work_area_offset)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
@@ -184,7 +162,8 @@ impl WindowManager {
}
match event {
WindowManagerEvent::Raise(_window) => {
WindowManagerEvent::Raise(window) => {
window.focus(false)?;
self.has_pending_raise_op = false;
}
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
@@ -259,7 +238,7 @@ impl WindowManager {
already_moved_window_handles.remove(&window.hwnd);
}
WindowManagerEvent::FocusChange(_, window) => {
self.update_focused_workspace(true, false)?;
self.update_focused_workspace(self.mouse_follows_focus, false)?;
let workspace = self.focused_workspace_mut()?;
if !workspace
@@ -283,27 +262,47 @@ impl WindowManager {
}
}
}
WindowManagerEvent::Show(_, window) | WindowManagerEvent::Manage(window) => {
let mut switch_to = None;
WindowManagerEvent::Show(_, window)
| WindowManagerEvent::Manage(window)
| WindowManagerEvent::Uncloak(_, window) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx =
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let mut needs_reconciliation = false;
for (i, monitors) in self.monitors().iter().enumerate() {
for (j, workspace) in monitors.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd) {
switch_to = Some((i, j));
}
}
}
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
// At this point we know we are going to send a notification to the workspace reconciliator
// So we get the topmost window returned by EnumWindows, which is almost always the window
// that has been selected by alt-tab
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
if let Some(first) =
alt_tab_windows.iter().find(|w| w.title().is_ok())
{
// If our record of this HWND hasn't been updated in over a minute
let mut instant = ALT_TAB_HWND_INSTANT.lock();
if instant.elapsed().gt(&Duration::from_secs(1)) {
// Update our record with the HWND we just found
ALT_TAB_HWND.store(Some(first.hwnd));
// Update the timestamp of our record
*instant = Instant::now();
}
}
}
if let Some((known_monitor_idx, known_workspace_idx)) = switch_to {
if self.focused_monitor_idx() != known_monitor_idx
|| self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx()
!= known_workspace_idx
{
self.focus_monitor(known_monitor_idx)?;
self.focus_workspace(known_workspace_idx)?;
return Ok(());
workspace_reconciliator::event_tx().send(
workspace_reconciliator::Notification {
monitor_idx: i,
workspace_idx: j,
},
)?;
needs_reconciliation = true;
}
}
}
@@ -312,6 +311,8 @@ impl WindowManager {
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.container_for_window(window.hwnd).is_some()
@@ -323,26 +324,28 @@ impl WindowManager {
);
window.hide();
return Ok(());
proceed = false;
}
}
}
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if proceed {
let behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if !workspace.contains_window(window.hwnd) {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
if !workspace.contains_window(window.hwnd) && !needs_reconciliation {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
}
}
}
}
@@ -582,110 +585,14 @@ impl WindowManager {
}
WindowManagerEvent::DisplayChange(..)
| WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::Uncloak(..) => {}
| WindowManagerEvent::Cloak(..) => {}
};
if !self.focused_workspace()?.tile() {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
if *self.focused_workspace()?.tile() && BORDER_ENABLED.load(Ordering::SeqCst) {
match event {
WindowManagerEvent::MoveResizeStart(_, _) => {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
WindowManagerEvent::MoveResizeEnd(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Hide(_, window)
| WindowManagerEvent::Uncloak(_, window)
| WindowManagerEvent::Minimize(_, window) => {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
let mut target_window = None;
let mut target_window_is_monocle = false;
if self
.focused_workspace()?
.floating_windows()
.iter()
.any(|w| w.hwnd == window.hwnd)
{
target_window = Option::from(window);
WindowsApi::raise_window(border.hwnd())?;
};
if let Some(monocle_container) = self.focused_workspace()?.monocle_container() {
if let Some(window) = monocle_container.focused_window() {
target_window = Option::from(*window);
target_window_is_monocle = true;
}
}
if target_window.is_none() {
match self.focused_container() {
// if there is no focused container, the desktop is empty
Err(..) => {
WindowsApi::hide_border_window(border.hwnd())?;
}
Ok(container) => {
if !(matches!(event, WindowManagerEvent::Minimize(_, _))
&& container.windows().len() == 1)
{
let container_size = self.focused_container()?.windows().len();
target_window = Option::from(*self.focused_window()?);
if target_window_is_monocle {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
} else if container_size > 1 {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_STACK.load(Ordering::SeqCst),
Ordering::SeqCst,
);
} else {
BORDER_COLOUR_CURRENT.store(
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
Ordering::SeqCst,
);
}
}
}
}
}
if let Some(target_window) = target_window {
let activate = BORDER_HIDDEN.load(Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
border.set_position(target_window, activate)?;
if activate {
BORDER_HIDDEN.store(false, Ordering::SeqCst);
}
}
}
_ => {}
}
}
// 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()?)?;
}
// If there are no more windows on the workspace, we shouldn't show the border window
if self.focused_workspace()?.containers().is_empty() {
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
BORDER_HIDDEN.store(true, Ordering::SeqCst);
}
tracing::trace!("updating list of known hwnds");
let mut known_hwnds = vec![];
for monitor in self.monitors() {
@@ -706,10 +613,14 @@ impl WindowManager {
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &known_hwnds)?;
notify_subscribers(&serde_json::to_string(&Notification {
let notification = Notification {
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
})?)?;
};
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::event_tx().send(border_manager::Notification)?;
tracing::info!("processed: {}", event.window().to_string());
Ok(())

View File

@@ -1,6 +1,5 @@
use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use std::time::Duration;
use color_eyre::eyre::Result;
use schemars::JsonSchema;
@@ -61,32 +60,13 @@ use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::STACKBAR_TAB_HEIGHT;
use crate::STACKBAR_TAB_WIDTH;
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_BY_BAR_HWNDS;
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct Stackbar {
pub(crate) hwnd: isize,
#[serde(skip)]
pub is_cloned: bool,
}
impl Drop for Stackbar {
fn drop(&mut self) {
if !self.is_cloned {
let _ = WindowsApi::close_window(self.hwnd());
}
}
}
impl Clone for Stackbar {
fn clone(&self) -> Self {
Self {
hwnd: self.hwnd,
is_cloned: true,
}
}
}
impl Stackbar {
unsafe extern "system" fn window_proc(
hwnd: HWND,
@@ -147,7 +127,7 @@ impl Stackbar {
lpfnWndProc: Some(Self::window_proc),
hInstance: h_module.into(),
lpszClassName: class_name,
hbrBackground: WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR),
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
@@ -175,14 +155,13 @@ impl Stackbar {
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd_sender.send(hwnd)?;
let mut msg = MSG::default();
while GetMessageW(&mut msg, hwnd, 0, 0).into() {
TranslateMessage(&msg);
DispatchMessageW(&msg);
std::thread::sleep(Duration::from_millis(10));
}
}
@@ -191,7 +170,6 @@ impl Stackbar {
Ok(Self {
hwnd: hwnd_receiver.recv()?.0,
..Default::default()
})
}

View File

@@ -1,4 +1,7 @@
use crate::border::Border;
use crate::border_manager;
use crate::border_manager::ZOrder;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::colour::Colour;
use crate::current_virtual_desktop;
use crate::monitor::Monitor;
@@ -7,15 +10,6 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
@@ -77,11 +71,13 @@ use uds_windows::UnixStream;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ActiveWindowBorderColours {
/// Border colour when the container contains a single window
pub single: Colour,
pub single: Option<Colour>,
/// Border colour when the container contains multiple windows
pub stack: Colour,
pub stack: Option<Colour>,
/// Border colour when the container is in monocle mode
pub monocle: Colour,
pub monocle: Option<Colour>,
/// Border colour when the container is unfocused
pub unfocused: Option<Colour>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -205,6 +201,12 @@ pub struct MonitorConfig {
/// Monitor-specific work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub work_area_offset: Option<Rect>,
/// Window based work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_based_work_area_offset: Option<Rect>,
/// Open window limit after which the window based work area offset will no longer be applied (default: 1)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_based_work_area_offset_limit: Option<isize>,
}
impl From<&Monitor> for MonitorConfig {
@@ -217,12 +219,14 @@ impl From<&Monitor> for MonitorConfig {
Self {
workspaces,
work_area_offset: value.work_area_offset(),
window_based_work_area_offset: value.window_based_work_area_offset(),
window_based_work_area_offset_limit: Some(value.window_based_work_area_offset_limit()),
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.24`
/// The `komorebi.json` static configuration file reference for `v0.1.25`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -265,6 +269,9 @@ pub struct StaticConfig {
/// Active window border style (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_style: Option<ActiveWindowBorderStyle>,
/// Active window border z-order (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_z_order: Option<ZOrder>,
/// Global default workspace padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
pub default_workspace_padding: Option<i32>,
@@ -304,22 +311,30 @@ pub struct StaticConfig {
/// Set display index preferences
#[serde(skip_serializing_if = "Option::is_none")]
pub display_index_preferences: Option<HashMap<usize, String>>,
/// Stackbar configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar: Option<StackbarConfig>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TabsConfig {
/// Width of a stackbar tab
width: Option<i32>,
/// Focused tab text colour
focused_text: Option<Colour>,
/// Unfocused tab text colour
unfocused_text: Option<Colour>,
/// Tab background colour
background: Option<Colour>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StackbarConfig {
/// Stackbar height
pub height: Option<i32>,
/// Stackbar mode
pub mode: Option<StackbarMode>,
/// Stackbar tab configuration options
pub tabs: Option<TabsConfig>,
}
@@ -376,21 +391,16 @@ impl From<&WindowManager> for StaticConfig {
}
}
let border_colours = if BORDER_COLOUR_SINGLE.load(Ordering::SeqCst) == 0 {
let border_colours = if border_manager::FOCUSED.load(Ordering::SeqCst) == 0 {
None
} else {
Option::from(ActiveWindowBorderColours {
single: Colour::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
stack: Colour::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
}),
monocle: Colour::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
} else {
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
}),
single: Option::from(Colour::from(border_manager::FOCUSED.load(Ordering::SeqCst))),
stack: Option::from(Colour::from(border_manager::STACK.load(Ordering::SeqCst))),
monocle: Option::from(Colour::from(border_manager::MONOCLE.load(Ordering::SeqCst))),
unfocused: Option::from(Colour::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
)),
})
};
@@ -405,11 +415,14 @@ impl From<&WindowManager> for StaticConfig {
focus_follows_mouse: value.focus_follows_mouse,
mouse_follows_focus: Option::from(value.mouse_follows_focus),
app_specific_configuration_path: None,
border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: Option::from(BORDER_OFFSET.load(Ordering::SeqCst)),
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
border_width: Option::from(border_manager::BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: Option::from(border_manager::BORDER_OFFSET.load(Ordering::SeqCst)),
active_window_border: Option::from(
border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
),
active_window_border_colours: border_colours,
active_window_border_style: Option::from(*ACTIVE_WINDOW_BORDER_STYLE.lock()),
active_window_border_style: Option::from(*STYLE.lock()),
active_window_border_z_order: Option::from(*Z_ORDER.lock()),
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
),
@@ -458,26 +471,33 @@ impl StaticConfig {
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
}
self.border_width.map_or_else(
|| {
BORDER_WIDTH.store(8, Ordering::SeqCst);
},
|width| {
BORDER_WIDTH.store(width, Ordering::SeqCst);
},
);
border_manager::BORDER_WIDTH.store(self.border_width.unwrap_or(8), Ordering::SeqCst);
border_manager::BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
if let Some(enabled) = &self.active_window_border {
border_manager::BORDER_ENABLED.store(*enabled, Ordering::SeqCst);
}
if let Some(colours) = &self.active_window_border_colours {
BORDER_COLOUR_SINGLE.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_CURRENT.store(u32::from(colours.single), Ordering::SeqCst);
BORDER_COLOUR_STACK.store(u32::from(colours.stack), Ordering::SeqCst);
BORDER_COLOUR_MONOCLE.store(u32::from(colours.monocle), Ordering::SeqCst);
if let Some(single) = colours.single {
border_manager::FOCUSED.store(u32::from(single), Ordering::SeqCst);
}
if let Some(stack) = colours.stack {
border_manager::STACK.store(u32::from(stack), Ordering::SeqCst);
}
if let Some(monocle) = colours.monocle {
border_manager::MONOCLE.store(u32::from(monocle), Ordering::SeqCst);
}
if let Some(unfocused) = colours.unfocused {
border_manager::UNFOCUSED.store(u32::from(unfocused), Ordering::SeqCst);
}
}
let active_window_border_style = self.active_window_border_style.unwrap_or_default();
*ACTIVE_WINDOW_BORDER_STYLE.lock() = active_window_border_style;
*STYLE.lock() = active_window_border_style;
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
@@ -682,6 +702,10 @@ impl StaticConfig {
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
m.set_work_area_offset(monitor.work_area_offset);
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
m.set_window_based_work_area_offset_limit(
monitor.window_based_work_area_offset_limit.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
ws.load_static_config(
@@ -710,12 +734,7 @@ impl StaticConfig {
}
if value.active_window_border == Some(true) {
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
Border::create("komorebi-border-window")?;
}
BORDER_ENABLED.store(true, Ordering::SeqCst);
wm.show_border()?;
border_manager::BORDER_ENABLED.store(true, Ordering::SeqCst);
}
Ok(())
@@ -742,6 +761,10 @@ impl StaticConfig {
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
m.set_work_area_offset(monitor.work_area_offset);
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
m.set_window_based_work_area_offset_limit(
monitor.window_based_work_area_offset_limit.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
ws.load_static_config(
@@ -769,16 +792,8 @@ impl StaticConfig {
}
}
if value.active_window_border == Some(true) {
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
Border::create("komorebi-border-window")?;
}
BORDER_ENABLED.store(true, Ordering::SeqCst);
wm.show_border()?;
} else {
BORDER_ENABLED.store(false, Ordering::SeqCst);
wm.hide_border()?;
if let Some(enabled) = value.active_window_border {
border_manager::BORDER_ENABLED.store(enabled, Ordering::SeqCst);
}
if let Some(val) = value.window_container_behaviour {

View File

@@ -14,7 +14,6 @@ use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use regex::Regex;
use schemars::JsonSchema;
use serde::ser::Error;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
@@ -39,7 +38,7 @@ use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema)]
pub struct Window {
pub hwnd: isize,
}
@@ -97,24 +96,23 @@ impl Serialize for Window {
"title",
&self
.title()
.map_err(|_| S::Error::custom("could not get window title"))?,
.unwrap_or_else(|_| String::from("could not get window title")),
)?;
state.serialize_field(
"exe",
&self
.exe()
.map_err(|_| S::Error::custom("could not get window exe"))?,
.unwrap_or_else(|_| String::from("could not get window exe")),
)?;
state.serialize_field(
"class",
&self
.class()
.map_err(|_| S::Error::custom("could not get window class"))?,
.unwrap_or_else(|_| String::from("could not get window class")),
)?;
state.serialize_field(
"rect",
&WindowsApi::window_rect(self.hwnd())
.map_err(|_| S::Error::custom("could not get window rect"))?,
&WindowsApi::window_rect(self.hwnd()).unwrap_or_default(),
)?;
state.end()
}
@@ -141,6 +139,10 @@ impl Window {
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
if WindowsApi::window_rect(self.hwnd())?.eq(layout) {
return Ok(());
}
let rect = *layout;
WindowsApi::position_window(self.hwnd(), &rect, top)
}

View File

@@ -39,7 +39,8 @@ use komorebi_core::Rect;
use komorebi_core::Sizing;
use komorebi_core::WindowContainerBehaviour;
use crate::border::Border;
use crate::border_manager;
use crate::border_manager::STYLE;
use crate::container::Container;
use crate::current_virtual_desktop;
use crate::load_configuration;
@@ -55,14 +56,6 @@ use crate::ActiveWindowBorderColours;
use crate::Colour;
use crate::Rgb;
use crate::WorkspaceRule;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_MONOCLE;
use crate::BORDER_COLOUR_SINGLE;
use crate::BORDER_COLOUR_STACK;
use crate::BORDER_ENABLED;
use crate::BORDER_HWND;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
@@ -153,15 +146,24 @@ pub struct GlobalState {
impl Default for GlobalState {
fn default() -> Self {
Self {
active_window_border_enabled: BORDER_ENABLED.load(Ordering::SeqCst),
active_window_border_enabled: border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
active_window_border_colours: ActiveWindowBorderColours {
single: Colour::Rgb(Rgb::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst))),
stack: Colour::Rgb(Rgb::from(BORDER_COLOUR_STACK.load(Ordering::SeqCst))),
monocle: Colour::Rgb(Rgb::from(BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst))),
single: Option::from(Colour::Rgb(Rgb::from(
border_manager::FOCUSED.load(Ordering::SeqCst),
))),
stack: Option::from(Colour::Rgb(Rgb::from(
border_manager::STACK.load(Ordering::SeqCst),
))),
monocle: Option::from(Colour::Rgb(Rgb::from(
border_manager::MONOCLE.load(Ordering::SeqCst),
))),
unfocused: Option::from(Colour::Rgb(Rgb::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
))),
},
active_window_border_style: *ACTIVE_WINDOW_BORDER_STYLE.lock(),
border_offset: BORDER_OFFSET.load(Ordering::SeqCst),
border_width: BORDER_WIDTH.load(Ordering::SeqCst),
active_window_border_style: *STYLE.lock(),
border_offset: border_manager::BORDER_OFFSET.load(Ordering::SeqCst),
border_width: border_manager::BORDER_WIDTH.load(Ordering::SeqCst),
stackbar_mode: *STACKBAR_MODE.lock(),
stackbar_focused_text_colour: Colour::Rgb(Rgb::from(
STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
@@ -286,28 +288,6 @@ impl WindowManager {
WindowsApi::load_workspace_information(&mut self.monitors)
}
#[tracing::instrument(skip(self))]
pub fn show_border(&self) -> Result<()> {
if self.focused_container().is_ok() {
let foreground = WindowsApi::foreground_window()?;
let foreground_window = Window { hwnd: foreground };
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.set_position(foreground_window, true)?;
WindowsApi::invalidate_border_rect()?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn hide_border(&self) -> Result<()> {
let focused = self.focused_window()?;
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
border.hide()?;
focused.focus(false)
}
#[tracing::instrument]
pub fn reload_configuration() {
tracing::info!("reloading configuration");
@@ -566,7 +546,7 @@ impl WindowManager {
target_workspace_idx: usize,
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
) -> () {
tracing::info!(
tracing::trace!(
"{} should be on monitor {}, workspace {}",
window_title,
target_monitor_idx,
@@ -733,6 +713,11 @@ impl WindowManager {
for monitor in self.monitors_mut() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
@@ -750,7 +735,7 @@ impl WindowManager {
}
}
workspace.update(&work_area, offset)?;
workspace.update(&work_area, offset, window_based_work_area_offset)?;
}
Ok(())
@@ -774,14 +759,11 @@ impl WindowManager {
pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> {
let mut hwnd = None;
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
if let Some(container_idx) = workspace.container_idx_from_current_point() {
if let Some(container) = workspace.containers().get(container_idx) {
if let Some(window) = container.focused_window() {
hwnd = Some(window.hwnd);
}
}
let workspace = self.focused_workspace()?;
if let Some(container_idx) = workspace.container_idx_from_current_point() {
if let Some(container) = workspace.containers().get(container_idx) {
if let Some(window) = container.focused_window() {
hwnd = Some(window.hwnd);
}
}
}
@@ -1208,6 +1190,7 @@ impl WindowManager {
let monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let workspace = monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
@@ -1228,6 +1211,11 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.add_container(container, workspace_idx)?;
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
}
target_monitor.load_focused_workspace(mouse_follows_focus)?;
target_monitor.update_focused_workspace(offset)?;
@@ -1350,6 +1338,11 @@ impl WindowManager {
// If there is nowhere to move on the current workspace, try to move it onto the monitor
// in that direction if there is one
None => {
// Don't do anything if the user has set the MoveBehaviour to NoOp
if matches!(self.cross_monitor_move_behaviour, MoveBehaviour::NoOp) {
return Ok(());
}
let target_monitor_idx = self
.monitor_idx_in_direction(direction)
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
@@ -1363,6 +1356,8 @@ impl WindowManager {
anyhow!("could not remove container at given origin index")
})?;
self.focused_workspace_mut()?.focus_previous_container();
// focus the target monitor
self.focus_monitor(target_monitor_idx)?;
@@ -1953,6 +1948,11 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
@@ -1972,7 +1972,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, offset)?;
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
@@ -2001,6 +2001,11 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
@@ -2022,7 +2027,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, offset)?;
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
@@ -2046,6 +2051,11 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
@@ -2063,7 +2073,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, offset)?;
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
@@ -2088,6 +2098,11 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
@@ -2104,7 +2119,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, offset)?;
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
@@ -2132,6 +2147,11 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor"))?;
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
monitor.window_based_work_area_offset_limit(),
monitor.window_based_work_area_offset(),
);
let focused_workspace_idx = monitor.focused_workspace_idx();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
@@ -2149,7 +2169,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, offset)?;
workspace.update(&work_area, offset, window_based_work_area_offset)?;
Ok(())
} else {
Ok(self.update_focused_workspace(false, false)?)
@@ -2322,6 +2342,13 @@ impl WindowManager {
None
}
pub fn focused_workspace_idx(&self) -> Result<usize> {
Ok(self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx())
}
pub fn focused_workspace(&self) -> Result<&Workspace> {
self.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?

View File

@@ -2,7 +2,6 @@ use std::collections::VecDeque;
use std::convert::TryFrom;
use std::ffi::c_void;
use std::mem::size_of;
use std::sync::atomic::Ordering;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::Error;
@@ -34,7 +33,6 @@ use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::EnumDisplayDevicesW;
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::MonitorFromPoint;
use windows::Win32::Graphics::Gdi::MonitorFromWindow;
use windows::Win32::Graphics::Gdi::Rectangle;
@@ -101,7 +99,6 @@ use windows::Win32::UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HWND_BOTTOM;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
@@ -142,8 +139,7 @@ use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::BORDER_HWND;
use crate::TRANSPARENCY_COLOUR;
use crate::Window;
pub enum WindowsResult<T, E> {
Err(E),
@@ -395,33 +391,9 @@ impl WindowsApi {
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
}
pub fn position_border_window(hwnd: HWND, layout: &Rect, activate: bool) -> Result<()> {
let flags = if activate {
SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE
} else {
SetWindowPosition::NO_ACTIVATE
};
// TODO(raggi): This leaves the window behind the active window, which
// can result e.g. single pixel window borders being invisible in the
// case of opaque window borders (e.g. EPIC Games Launcher). Ideally
// we'd be able to pass a parent window to place ourselves just in front
// of, however the SetWindowPos API explicitly ignores that parameter
// unless the window being positioned is being activated - and we don't
// want to activate the border window here. We can hopefully find a
// better workaround in the future.
// The trade-off chosen prevents the border window from sitting over the
// top of other pop-up dialogs such as a file picker dialog from
// Firefox. When adjusting this in the future, it's important to check
// those dialog cases.
Self::set_window_pos(hwnd, layout, HWND_TOP, flags.bits())
}
pub fn hide_border_window(hwnd: HWND) -> Result<()> {
let flags = SetWindowPosition::HIDE_WINDOW;
let position = HWND_BOTTOM;
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
pub fn set_border_pos(hwnd: HWND, layout: &Rect, position: HWND) -> Result<()> {
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
Self::set_window_pos(hwnd, layout, position, flags.bits())
}
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
@@ -522,6 +494,16 @@ impl WindowsApi {
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
}
pub fn alt_tab_windows() -> Result<Vec<Window>> {
let mut hwnds = vec![];
Self::enum_windows(
Some(windows_callbacks::alt_tab_windows),
&mut hwnds as *mut Vec<Window> as isize,
)?;
Ok(hwnds)
}
#[allow(dead_code)]
pub fn top_visible_window() -> Result<isize> {
let hwnd = Self::top_window()?;
@@ -963,7 +945,7 @@ impl WindowsApi {
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd
}
@@ -1000,12 +982,6 @@ impl WindowsApi {
.process()
}
pub fn invalidate_border_rect() -> Result<()> {
unsafe { InvalidateRect(HWND(BORDER_HWND.load(Ordering::SeqCst)), None, false) }
.ok()
.process()
}
pub fn alt_is_pressed() -> bool {
let state = unsafe { GetKeyState(i32::from(VK_MENU.0)) };
#[allow(clippy::cast_sign_loss)]

View File

@@ -1,36 +1,21 @@
use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use widestring::U16CStr;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::SPI_ICONVERTICALSPACING;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
use crate::container::Container;
@@ -42,15 +27,8 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use crate::ACTIVE_WINDOW_BORDER_STYLE;
use crate::BORDER_COLOUR_CURRENT;
use crate::BORDER_RECT;
use crate::BORDER_WIDTH;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::TRANSPARENCY_COLOUR;
use crate::WINDOWS_11;
use komorebi_core::ActiveWindowBorderStyle;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
@@ -174,6 +152,26 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let is_visible = WindowsApi::is_window_visible(hwnd);
let is_window = WindowsApi::is_window(hwnd);
let is_minimized = WindowsApi::is_iconic(hwnd);
if is_visible && is_window && !is_minimized {
let window = Window { hwnd: hwnd.0 };
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
if should_manage {
windows.push(window);
}
}
}
true.into()
}
pub extern "system" fn win_event_hook(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
@@ -204,62 +202,6 @@ pub extern "system" fn win_event_hook(
.expect("could not send message on winevent_listener::event_tx");
}
pub extern "system" fn border_window(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_PAINT => {
let border_rect = *BORDER_RECT.lock();
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
let hpen = CreatePen(
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(BORDER_COLOUR_CURRENT.load(Ordering::SeqCst)),
);
let hbrush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
// TODO(raggi): this is approximately the correct curvature for
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
// often the bottom right has a different shape. Furthermore if
// the window was made with DWMWCP_ROUNDSMALL then this is the
// wrong size. In the future we should read the DWM properties
// of windows and attempt to match appropriately.
match *ACTIVE_WINDOW_BORDER_STYLE.lock() {
ActiveWindowBorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, border_rect.right, border_rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
}
}
ActiveWindowBorderStyle::Rounded => {
RoundRect(hdc, 0, 0, border_rect.right, border_rect.bottom, 20, 20);
}
ActiveWindowBorderStyle::Square => {
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
}
}
EndPaint(window, &ps);
ValidateRect(window, None);
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
pub extern "system" fn hidden_window(
window: HWND,
message: u32,

View File

@@ -20,14 +20,14 @@ use komorebi_core::Layout;
use komorebi_core::OperationDirection;
use komorebi_core::Rect;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::container::Container;
use crate::ring::Ring;
use crate::static_config::WorkspaceConfig;
use crate::window::Window;
use crate::window::WindowDetails;
use crate::windows_api::WindowsApi;
use crate::BORDER_OFFSET;
use crate::BORDER_WIDTH;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
@@ -131,6 +131,8 @@ impl Workspace {
}
self.set_layout_rules(all_rules);
self.tile = true;
}
if let Some(layout_rules) = &config.custom_layout_rules {
@@ -139,6 +141,8 @@ impl Workspace {
let rule = CustomLayout::from_path(pathbuf)?;
rules.push((*count, Layout::Custom(rule)));
}
self.tile = true;
}
Ok(())
@@ -215,13 +219,21 @@ impl Workspace {
Ok(())
}
pub fn update(&mut self, work_area: &Rect, offset: Option<Rect>) -> Result<()> {
pub fn update(
&mut self,
work_area: &Rect,
work_area_offset: Option<Rect>,
window_based_work_area_offset: (isize, Option<Rect>),
) -> Result<()> {
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
return Ok(());
}
let (window_based_work_area_offset_limit, window_based_work_area_offset) =
window_based_work_area_offset;
let container_padding = self.container_padding();
let mut adjusted_work_area = offset.map_or_else(
let mut adjusted_work_area = work_area_offset.map_or_else(
|| *work_area,
|offset| {
let mut with_offset = *work_area;
@@ -234,6 +246,21 @@ impl Workspace {
},
);
if self.containers().len() <= window_based_work_area_offset_limit as usize {
adjusted_work_area = window_based_work_area_offset.map_or_else(
|| adjusted_work_area,
|offset| {
let mut with_offset = adjusted_work_area;
with_offset.left += offset.left;
with_offset.top += offset.top;
with_offset.right -= offset.right;
with_offset.bottom -= offset.bottom;
with_offset
},
);
}
adjusted_work_area.add_padding(self.workspace_padding().unwrap_or_default());
self.enforce_resize_constraints();
@@ -876,7 +903,7 @@ impl Workspace {
self.containers_mut().remove(focused_idx);
self.resize_dimensions_mut().remove(focused_idx);
if focused_idx == self.containers().len() {
if focused_idx == self.containers().len() && focused_idx != 0 {
self.focus_container(focused_idx - 1);
}
} else {
@@ -894,6 +921,17 @@ impl Workspace {
fn enforce_resize_constraints(&mut self) {
match self.layout {
Layout::Default(DefaultLayout::BSP) => self.enforce_resize_constraints_for_bsp(),
Layout::Default(DefaultLayout::Columns) => self.enforce_resize_for_columns(),
Layout::Default(DefaultLayout::Rows) => self.enforce_resize_for_rows(),
Layout::Default(DefaultLayout::VerticalStack) => {
self.enforce_resize_for_vertical_stack();
}
Layout::Default(DefaultLayout::RightMainVerticalStack) => {
self.enforce_resize_for_right_vertical_stack();
}
Layout::Default(DefaultLayout::HorizontalStack) => {
self.enforce_resize_for_horizontal_stack();
}
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
self.enforce_resize_for_ultrawide();
}
@@ -927,6 +965,146 @@ impl Workspace {
}
}
fn enforce_resize_for_columns(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
let len = resize_dimensions.len();
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
if let Some(rect) = rect {
rect.top = 0;
rect.bottom = 0;
if i == 0 {
rect.left = 0;
}
if i == len - 1 {
rect.right = 0;
}
}
}
}
}
}
fn enforce_resize_for_rows(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
let len = resize_dimensions.len();
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
if let Some(rect) = rect {
rect.left = 0;
rect.right = 0;
if i == 0 {
rect.top = 0;
}
if i == len - 1 {
rect.bottom = 0;
}
}
}
}
}
}
fn enforce_resize_for_vertical_stack(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
// Single window can not be resized at all
0 | 1 => self.enforce_no_resize(),
_ => {
// Zero is actually on the left
if let Some(mut left) = resize_dimensions[0] {
left.top = 0;
left.bottom = 0;
left.left = 0;
}
// Handle stack on the right
let stack_size = resize_dimensions[1..].len();
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
if let Some(rect) = rect {
// No containers can resize to the right
rect.right = 0;
// First container in stack cant resize up
if i == 0 {
rect.top = 0;
} else if i == stack_size - 1 {
// Last cant be resized to the bottom
rect.bottom = 0;
}
}
}
}
}
}
fn enforce_resize_for_right_vertical_stack(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
// Single window can not be resized at all
0 | 1 => self.enforce_no_resize(),
_ => {
// Zero is actually on the right
if let Some(mut left) = resize_dimensions[1] {
left.top = 0;
left.bottom = 0;
left.right = 0;
}
// Handle stack on the right
let stack_size = resize_dimensions[1..].len();
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
if let Some(rect) = rect {
// No containers can resize to the left
rect.left = 0;
// First container in stack cant resize up
if i == 0 {
rect.top = 0;
} else if i == stack_size - 1 {
// Last cant be resized to the bottom
rect.bottom = 0;
}
}
}
}
}
}
fn enforce_resize_for_horizontal_stack(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
if let Some(mut left) = resize_dimensions[0] {
left.top = 0;
left.left = 0;
left.right = 0;
}
let stack_size = resize_dimensions[1..].len();
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
if let Some(rect) = rect {
rect.bottom = 0;
if i == 0 {
rect.left = 0;
}
if i == stack_size - 1 {
rect.right = 0;
}
}
}
}
}
}
fn enforce_resize_for_ultrawide(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
@@ -1221,7 +1399,7 @@ impl Workspace {
vec
}
fn focus_previous_container(&mut self) {
pub fn focus_previous_container(&mut self) {
let focused_idx = self.focused_container_idx();
if focused_idx != 0 {

View File

@@ -0,0 +1,116 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::WindowManager;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone)]
pub struct Notification {
pub monitor_idx: usize,
pub workspace_idx: usize,
}
pub static ALT_TAB_HWND: AtomicCell<Option<isize>> = AtomicCell::new(None);
lazy_static! {
pub static ref ALT_TAB_HWND_INSTANT: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
}
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
}
pub fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
pub fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
let arc = wm.clone();
for notification in receiver {
tracing::info!("running reconciliation");
let mut wm = wm.lock();
let focused_monitor_idx = wm.focused_monitor_idx();
let focused_workspace_idx =
wm.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let updated_pair = (notification.monitor_idx, notification.workspace_idx);
if focused_pair != updated_pair {
wm.focus_monitor(notification.monitor_idx)?;
let mouse_follows_focus = wm.mouse_follows_focus;
if let Some(monitor) = wm.focused_monitor_mut() {
monitor.focus_workspace(notification.workspace_idx)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
}
// Drop our lock on the window manager state here to not slow down updates
drop(wm);
// Check if there was an alt-tab across workspaces in the last second
if let Some(hwnd) = ALT_TAB_HWND.load() {
if ALT_TAB_HWND_INSTANT
.lock()
.elapsed()
.lt(&Duration::from_secs(1))
{
// Sleep for 100 millis to let other events pass
std::thread::sleep(Duration::from_millis(100));
tracing::info!("focusing alt-tabbed window");
// Take a new lock on the wm and try to focus the container with
// the recorded HWND from the alt-tab
let mut wm = arc.lock();
if let Ok(workspace) = wm.focused_workspace_mut() {
// Regardless of if this fails, we need to get past this part
// to unblock the border manager below
let _ = workspace.focus_container_by_window(hwnd);
}
// Unblock the border manager
ALT_TAB_HWND.store(None);
// Send a notification to the border manager to update the borders
border_manager::event_tx().send(border_manager::Notification)?;
}
}
}
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.25-dev.0"
version = "0.1.26-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]

View File

@@ -1257,9 +1257,15 @@ fn main() -> Result<()> {
let subcommands = cli.get_subcommands_mut();
std::fs::create_dir_all("docs/cli")?;
let ignore = [
"docgen",
"alt-focus-hack",
"identify-border-overflow-application",
];
for cmd in subcommands {
let name = cmd.get_name().to_string();
if name != "docgen" {
if !ignore.contains(&name.as_str()) {
let help_text = cmd.render_long_help().to_string();
let outpath = format!("docs/cli/{name}.md");
let markdown = format!("# {name}\n\n```\n{help_text}\n```");

View File

@@ -39,166 +39,173 @@ theme:
- search.share
- search.suggest
- toc.follow
markdown_extensions:
- admonition
- pymdownx.highlight
- pymdownx.superfences
plugins:
- macros
- search
nav:
- Komorebi:
- index.md
- Design: design.md
- index.md
- Design: design.md
- Getting started:
- Installation: installation.md
- Example configurations: example-configurations.md
- Installation: installation.md
- Example configurations: example-configurations.md
- Troubleshooting: troubleshooting.md
- Common workflows:
- common-workflows/komorebi-config-home.md
- common-workflows/active-window-border.md
- common-workflows/stackbar.md
- common-workflows/remove-gaps.md
- common-workflows/ignore-windows.md
- common-workflows/force-manage-windows.md
- common-workflows/tray-and-multi-window-applications.md
- common-workflows/focus-follows-mouse.md
- common-workflows/mouse-follows-focus.md
- common-workflows/custom-layouts.md
- common-workflows/dynamic-layout-switching.md
# - common-workflows/autohotkey.md
- common-workflows/komorebi-config-home.md
- common-workflows/active-window-border.md
- common-workflows/stackbar.md
- common-workflows/remove-gaps.md
- common-workflows/ignore-windows.md
- common-workflows/force-manage-windows.md
- common-workflows/tray-and-multi-window-applications.md
- common-workflows/focus-follows-mouse.md
- common-workflows/mouse-follows-focus.md
- common-workflows/custom-layouts.md
- common-workflows/dynamic-layout-switching.md
# - common-workflows/autohotkey.md
- Release notes:
- release/v0-1-22.md
- release/v0-1-22.md
- Configuration reference: https://komorebi.lgug2z.com/schema
- CLI reference:
- cli/quickstart.md
- cli/start.md
- cli/stop.md
- cli/check.md
- cli/configuration.md
- cli/whkdrc.md
- cli/state.md
- cli/visible-windows.md
- cli/query.md
- cli/subscribe-socket.md
- cli/unsubscribe-socket.md
- cli/subscribe-pipe.md
- cli/unsubscribe-pipe.md
- cli/log.md
- cli/quick-save-resize.md
- cli/quick-load-resize.md
- cli/save-resize.md
- cli/load-resize.md
- cli/focus.md
- cli/move.md
- cli/minimize.md
- cli/close.md
- cli/force-focus.md
- cli/cycle-focus.md
- cli/cycle-move.md
- cli/stack.md
- cli/resize-edge.md
- cli/resize-axis.md
- cli/unstack.md
- cli/cycle-stack.md
- cli/move-to-monitor.md
- cli/cycle-move-to-monitor.md
- cli/move-to-workspace.md
- cli/move-to-named-workspace.md
- cli/cycle-move-to-workspace.md
- cli/send-to-monitor.md
- cli/cycle-send-to-monitor.md
- cli/send-to-workspace.md
- cli/send-to-named-workspace.md
- cli/cycle-send-to-workspace.md
- cli/send-to-monitor-workspace.md
- cli/focus-monitor.md
- cli/focus-last-workspace.md
- cli/focus-workspace.md
- cli/focus-workspaces.md
- cli/focus-monitor-workspace.md
- cli/focus-named-workspace.md
- cli/cycle-monitor.md
- cli/cycle-workspace.md
- cli/move-workspace-to-monitor.md
- cli/swap-workspaces-with-monitor.md
- cli/new-workspace.md
- cli/resize-delta.md
- cli/invisible-borders.md
- cli/global-work-area-offset.md
- cli/monitor-work-area-offset.md
- cli/focused-workspace-container-padding.md
- cli/focused-workspace-padding.md
- cli/adjust-container-padding.md
- cli/adjust-workspace-padding.md
- cli/change-layout.md
- cli/cycle-layout.md
- cli/load-custom-layout.md
- cli/flip-layout.md
- cli/promote.md
- cli/promote-focus.md
- cli/retile.md
- cli/monitor-index-preference.md
- cli/display-index-preference.md
- cli/ensure-workspaces.md
- cli/ensure-named-workspaces.md
- cli/container-padding.md
- cli/named-workspace-container-padding.md
- cli/workspace-padding.md
- cli/named-workspace-padding.md
- cli/workspace-layout.md
- cli/named-workspace-layout.md
- cli/workspace-custom-layout.md
- cli/named-workspace-custom-layout.md
- cli/workspace-layout-rule.md
- cli/named-workspace-layout-rule.md
- cli/workspace-custom-layout-rule.md
- cli/named-workspace-custom-layout-rule.md
- cli/clear-workspace-layout-rules.md
- cli/clear-named-workspace-layout-rules.md
- cli/workspace-tiling.md
- cli/named-workspace-tiling.md
- cli/workspace-name.md
- cli/toggle-window-container-behaviour.md
- cli/toggle-pause.md
- cli/toggle-tiling.md
- cli/toggle-float.md
- cli/toggle-monocle.md
- cli/toggle-maximize.md
- cli/restore-windows.md
- cli/manage.md
- cli/unmanage.md
- cli/reload-configuration.md
- cli/watch-configuration.md
- cli/complete-configuration.md
- cli/window-hiding-behaviour.md
- cli/cross-monitor-move-behaviour.md
- cli/toggle-cross-monitor-move-behaviour.md
- cli/unmanaged-window-operation-behaviour.md
- cli/float-rule.md
- cli/manage-rule.md
- cli/initial-workspace-rule.md
- cli/initial-named-workspace-rule.md
- cli/workspace-rule.md
- cli/named-workspace-rule.md
- cli/identify-object-name-change-application.md
- cli/identify-tray-application.md
- cli/identify-layered-application.md
- cli/remove-title-bar.md
- cli/toggle-title-bars.md
- cli/active-window-border.md
- cli/active-window-border-colour.md
- cli/active-window-border-width.md
- cli/active-window-border-offset.md
- cli/focus-follows-mouse.md
- cli/toggle-focus-follows-mouse.md
- cli/mouse-follows-focus.md
- cli/toggle-mouse-follows-focus.md
- cli/ahk-library.md
- cli/ahk-app-specific-configuration.md
- cli/pwsh-app-specific-configuration.md
- cli/format-app-specific-configuration.md
- cli/fetch-app-specific-configuration.md
- cli/application-specific-configuration-schema.md
- cli/notification-schema.md
- cli/socket-schema.md
- cli/static-config-schema.md
- cli/generate-static-config.md
- cli/enable-autostart.md
- cli/disable-autostart.md
- cli/quickstart.md
- cli/start.md
- cli/stop.md
- cli/check.md
- cli/configuration.md
- cli/whkdrc.md
- cli/state.md
- cli/global-state.md
- cli/visible-windows.md
- cli/query.md
- cli/subscribe-socket.md
- cli/unsubscribe-socket.md
- cli/subscribe-pipe.md
- cli/unsubscribe-pipe.md
- cli/log.md
- cli/quick-save-resize.md
- cli/quick-load-resize.md
- cli/save-resize.md
- cli/load-resize.md
- cli/focus.md
- cli/move.md
- cli/minimize.md
- cli/close.md
- cli/force-focus.md
- cli/cycle-focus.md
- cli/cycle-move.md
- cli/stack.md
- cli/resize-edge.md
- cli/resize-axis.md
- cli/unstack.md
- cli/cycle-stack.md
- cli/move-to-monitor.md
- cli/cycle-move-to-monitor.md
- cli/move-to-workspace.md
- cli/move-to-named-workspace.md
- cli/cycle-move-to-workspace.md
- cli/send-to-monitor.md
- cli/cycle-send-to-monitor.md
- cli/send-to-workspace.md
- cli/send-to-named-workspace.md
- cli/cycle-send-to-workspace.md
- cli/send-to-monitor-workspace.md
- cli/move-to-monitor-workspace.md
- cli/focus-monitor.md
- cli/focus-last-workspace.md
- cli/focus-workspace.md
- cli/focus-workspaces.md
- cli/focus-monitor-workspace.md
- cli/focus-named-workspace.md
- cli/cycle-monitor.md
- cli/cycle-workspace.md
- cli/move-workspace-to-monitor.md
- cli/swap-workspaces-with-monitor.md
- cli/new-workspace.md
- cli/resize-delta.md
- cli/invisible-borders.md
- cli/global-work-area-offset.md
- cli/monitor-work-area-offset.md
- cli/focused-workspace-container-padding.md
- cli/focused-workspace-padding.md
- cli/adjust-container-padding.md
- cli/adjust-workspace-padding.md
- cli/change-layout.md
- cli/cycle-layout.md
- cli/load-custom-layout.md
- cli/flip-layout.md
- cli/promote.md
- cli/promote-focus.md
- cli/retile.md
- cli/monitor-index-preference.md
- cli/display-index-preference.md
- cli/ensure-workspaces.md
- cli/ensure-named-workspaces.md
- cli/container-padding.md
- cli/named-workspace-container-padding.md
- cli/workspace-padding.md
- cli/named-workspace-padding.md
- cli/workspace-layout.md
- cli/named-workspace-layout.md
- cli/workspace-custom-layout.md
- cli/named-workspace-custom-layout.md
- cli/workspace-layout-rule.md
- cli/named-workspace-layout-rule.md
- cli/workspace-custom-layout-rule.md
- cli/named-workspace-custom-layout-rule.md
- cli/clear-workspace-layout-rules.md
- cli/clear-named-workspace-layout-rules.md
- cli/workspace-tiling.md
- cli/named-workspace-tiling.md
- cli/workspace-name.md
- cli/toggle-window-container-behaviour.md
- cli/toggle-pause.md
- cli/toggle-tiling.md
- cli/toggle-float.md
- cli/toggle-monocle.md
- cli/toggle-maximize.md
- cli/restore-windows.md
- cli/manage.md
- cli/unmanage.md
- cli/reload-configuration.md
- cli/watch-configuration.md
- cli/complete-configuration.md
- cli/window-hiding-behaviour.md
- cli/cross-monitor-move-behaviour.md
- cli/toggle-cross-monitor-move-behaviour.md
- cli/unmanaged-window-operation-behaviour.md
- cli/float-rule.md
- cli/manage-rule.md
- cli/initial-workspace-rule.md
- cli/initial-named-workspace-rule.md
- cli/workspace-rule.md
- cli/named-workspace-rule.md
- cli/identify-object-name-change-application.md
- cli/identify-tray-application.md
- cli/identify-layered-application.md
- cli/remove-title-bar.md
- cli/toggle-title-bars.md
- cli/active-window-border.md
- cli/active-window-border-colour.md
- cli/active-window-border-width.md
- cli/active-window-border-offset.md
- cli/focus-follows-mouse.md
- cli/toggle-focus-follows-mouse.md
- cli/mouse-follows-focus.md
- cli/toggle-mouse-follows-focus.md
- cli/ahk-library.md
- cli/ahk-app-specific-configuration.md
- cli/pwsh-app-specific-configuration.md
- cli/format-app-specific-configuration.md
- cli/fetch-app-specific-configuration.md
- cli/application-specific-configuration-schema.md
- cli/notification-schema.md
- cli/socket-schema.md
- cli/static-config-schema.md
- cli/generate-static-config.md
- cli/enable-autostart.md
- cli/disable-autostart.md

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.20`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.25`",
"type": "object",
"properties": {
"active_window_border": {
@@ -775,7 +775,8 @@
"VerticalStack",
"HorizontalStack",
"UltrawideVerticalStack",
"Grid"
"Grid",
"RightMainVerticalStack"
]
},
"layout_rules": {
@@ -790,7 +791,8 @@
"VerticalStack",
"HorizontalStack",
"UltrawideVerticalStack",
"Grid"
"Grid",
"RightMainVerticalStack"
]
}
},
@@ -942,13 +944,16 @@
"format": "int32"
},
"stackbar": {
"description": "Stackbar configuration options",
"type": "object",
"properties": {
"height": {
"description": "Stackbar height",
"type": "integer",
"format": "int32"
},
"mode": {
"description": "Stackbar mode",
"type": "string",
"enum": [
"Always",
@@ -957,9 +962,11 @@
]
},
"tabs": {
"description": "Stackbar tab configuration options",
"type": "object",
"properties": {
"background": {
"description": "Tab background colour",
"anyOf": [
{
"description": "Colour represented as RGB",
@@ -997,6 +1004,7 @@
]
},
"focused_text": {
"description": "Focused tab text colour",
"anyOf": [
{
"description": "Colour represented as RGB",
@@ -1034,6 +1042,7 @@
]
},
"unfocused_text": {
"description": "Unfocused tab text colour",
"anyOf": [
{
"description": "Colour represented as RGB",
@@ -1071,6 +1080,7 @@
]
},
"width": {
"description": "Width of a stackbar tab",
"type": "integer",
"format": "int32"
}

11
shell.nix Normal file
View File

@@ -0,0 +1,11 @@
{pkgs ? import <nixpkgs> {}}:
with pkgs;
mkShell {
name = "komorebi";
buildInputs = [
python311Packages.mkdocs-material
python311Packages.mkdocs-macros
python311Packages.setuptools
];
}