Compare commits

...

154 Commits

Author SHA1 Message Date
LGUG2Z
8ceed09c20 fix(wm): dynamically reserve monitor ring space
This commit makes a small change to dynamically keep reserving space in
the VecDeque that backs Ring<Monitor> until an index preference can be
contained within the current length.
2024-05-23 16:58:54 -07:00
LGUG2Z
1712042dda chore(dev): begin v0.1.27-dev 2024-05-23 16:47:26 -07:00
LGUG2Z
3556f38469 chore(release): v0.1.26 2024-05-22 15:36:20 -07:00
LGUG2Z
62770033f2 fix(wm): make close + min op on foreground hwnd
This commit changes the handlers for the Close and Minimize
SocketMessages to operate on the output of
WindowsApi::foreground_window, without checking the window manager state
as it was doing previously.

This will allow the commands to operate on any kind of managed or
unmanaged window, and the appropriate WinEvent will be emitted by the
closed window for the window manager state to be updated when the
WinEvent goes through process_event.

fix #839
2024-05-22 15:33:03 -07:00
LGUG2Z
e294dbbe93 feat(cli): rename and deprecation feedback
This commit adds feedback about renamed and deprecated configuration
options in a user's komorebi.json file when the 'start' command is run.
2024-05-22 12:26:11 -07:00
LGUG2Z
47f0ab1ef3 feat(wm): handle OBJECT_NAMECHANGE for all apps
This commit ensures that EVENT_OBJECT_NAMECHANGE is handled for all
windows.

Previously this was mapped to WindowManagerEvent::Show, as this is the
event that apps like Firefox and JetBrains IDEs sent on launch instead
of EVENT_OBJECT_SHOW like normal apps.

Now that we are using EVENT_OBJECT_NAMECHANGE to update titles on
stackbar tabs, when a window which is not in the whitelist of
object_name_change_applications sends this event, it will be handled by
the new WindowManagerEvent::TitleUpdate variant.

This ensures that a stackbar_manager::Notification is sent at the end of
process_event to update stackbar tabs when application titles are
changing.

resolve #842
2024-05-22 11:37:49 -07:00
LGUG2Z
c4d62fc4f6 feat(wm): run orphan reaper in a dedicated thread
Until now the orphan window/container reaper has always run on every
WinEvent. Unfortunately Windows Terminal does not sent a WinEvent when
it is closed.

This is a problem for the new border_manager module which draws and
destroys borders based on notifications sent to it after WinEvents and
SocketMessages have been processed.

Since Windows Terminal is not sending a WinEvent on close, this means
that user interaction is required to remove the ghost border that gets
left behind.

This commit starts a separate thread for the reaper where it runs once
every second in a loop.

This is quite wasteful and definitely not something I wanted to
implement, but a temporary solution is needed given the popularity of
the buggy application in question.

An issue on the Windows Terminal tracker has been opened here:
https://github.com/microsoft/terminal/issues/17298
2024-05-21 19:27:58 -07:00
LGUG2Z
69680b4238 fix(stackbar): destroy stackbars on ws change
This commit ensures that whenever we receive a
stackbar_manager::Notification any stackbars not associated with the
current workspace on each monitor are destroyed.

fix #838
2024-05-21 08:23:05 -07:00
LGUG2Z
0dc17e9cb3 fix(wm): restore containers when closing monocle
This commit fixes a regression introduced by hiding other containers
when monocle is enabled. When the monocle container is closed, other
containers on the workspace will now be restored.

re #834
2024-05-20 08:24:55 -07:00
LGUG2Z
0f44efaa82 docs(wm): add komorebi-gui binary, update mkdocs 2024-05-19 16:37:35 -07:00
LGUG2Z
05af7ce16a feat(gui): add the komorebi-gui debug tool
This commit adds the komorebi-gui debug tool build with egui and eframe.

This tool was built from scratch in a YouTube mini-series which can be
found here: https://www.youtube.com/watch?v=zZKjBMt4kZ4

The most interesting part of this tool right now is the ability to view
debug information about each window as it goes through the rules engine.

While it's possible to change runtime configuration options with this
tool, it is not yet possible to write those changes out to the
configuration file.
2024-05-19 14:27:18 -07:00
LGUG2Z
92447723d2 fix(wm): respect horizontal focus from monocle
This commit ensures that horizontal focus moves onto other monitors from
a monocle container are respected (ie. we don't try moving left/right
within the workspace on the focused monitor).

Additionally, if the user tries to alt-tab a window to the foreground on
a workspace where a monocle container exists, the window will flash
before being hidden behind the monocle container as a visual cue that
monocle mode needs to be disabled to access that window.

This is in contrast to the current behaviour where that window floats on
top of the monocle container in a somewhat broken state.

re #834
2024-05-19 12:45:35 -07:00
LGUG2Z
2a45f981e6 feat(stackbar): add stackbar manager module
This commit removes all stackbar-related code from Container, Workspace,
process_command, process_event etc. and centralizes it in the new
stackbar_manager module.

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

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

Global state related to stackbars has also been moved into the
stackbar_manager module, which also tracks the state of stackbar objects
(STACKBAR_STATE), mappings between stackbars and containers
(STACKBARS_CONTAINERS) and the mappings between stackbars and monitors
(STACKBARS_MONITORS).

A number of edge cases around stackbar behaviour have been addressed in
this commit (re #832), and stackbars now respect the "border_style"
configuration option.
2024-05-19 10:55:40 -07:00
LGUG2Z
a29ab4cfb3 feat(wm): add monitor reconciliator module
This commit adds the monitor_reconciliator module which uses a tightly
bounded channel (cap: 1) to handle monitor connection and disconnection
events, as well as resolution and work area change events.

Before, all this logic lived in a the WindowManager.reconcile_monitors
function, which ran on pretty much every process_event iteration, and
sometimes led to undesirable behaviour, but now the logic is split up to
only run when the appropriate notifications are dispatched from the
hidden window which listens for monitor and display-related events.

The monitor cache has been moved out of WindowManager and into the
monitor_reconciliator module, and in addition to the previous behaviour
of attempting to cache monitors which had been identified as
disconnected, now when the static configuration file is loaded, if the
user has set display_index_preferences, the device IDs will be used to
pre-populate the cache for the event where a known monitor is connected
later in a session.

The monitor cache itself now uses the unique device ID as a key rather
than the hmonitor which is known to be inconsistent.

This commit also delegates all display monitor-related Win32 calls to
the "win32-display-data" crate, which was extracted from the larger
"brightness" crate for its use in komorebi.

As a result of these changes, "device" and "device_id" on Monitor have
been changed from Option<String> to String types, as failures in
retrieving these values with directly attached monitors has not been
possible to reproduce. However, it remains to be seen if this will
adversely impact users who use display docks which may prevent display
monitor device IDs from being read and stored by the operating system.

WindowManagerEvent::DisplayChange has been removed in favour of
the monitor_reconciliator::Notification enum, as these events are no
longer being handled in process_events.

Attempts are now made to eagerly update hmonitors both within the
monitor_reconciliator loop on DisplayConnectionChange notifications and
when failing to find a matching hmonitor in functions like
monitor_idx_from_current_pos and monitor_idx_from_window.
2024-05-19 06:26:45 -07:00
LGUG2Z
27cd1736aa fix(wm): smooth transitions to monocle workspaces
This commit adds hiding and restoring of other containers on a workspace
with monocle on/off, and exits early when a monocle container is found
on workspace restores to avoid flashing of other containers before the
workspace focus operation completes.

Focus is also restored when focusing a monocle container on another
monitor as part of a cross-monitor focus operation.

The border rendering for monocle containers has also been tightened up.

re #819
2024-05-18 09:53:10 -07:00
LGUG2Z
d2470b1f08 fix(cli): have log command lookup file w/ timestamp 2024-05-17 16:18:21 -07:00
LGUG2Z
835472d739 fix(wm): address monocle mode visual artifacts
This commit addresses two visual artifacts with monocle mode:

* Flashing of background windows when switching to a monocle container
  on another monitor is now gone
* Stackbars are automatically disabled whenever a container enters
  monocle mode

re #819
2024-05-17 15:54:40 -07:00
LGUG2Z
bceb28de37 fix(wm): various monocle container regressions
This commit fixes a number of monocle container-related regressions.

* Monocle container on one monitor preventing border updates on another
* Cross-monitor focus changes towards a monitor w/ a monocle container
* Cross-monitor move towards a monitor w/ a monocle container

re #819
2024-05-17 08:42:56 -07:00
LGUG2Z
fff7b5c147 feat(stackbar): update title labels in realtime
This commit handles the EVENT_OBJECT_NAMECHANGE WinEvent which is
emitted when window titles change to update Stackbar labels in real-time
when StackbarLabel::Title is used.

re #826
2024-05-16 16:23:09 -07:00
LGUG2Z
1e63947ae3 fix(wm): uw-vertical-stack stacking regression
This commit fixes a small regression that was introduced with the
addition of the Grid layout, where stacking right from index 0 on the
UltrawideVerticalStack layout would actually end up stacking to the
left.
2024-05-16 14:32:03 -07:00
LGUG2Z
d5f4f916be feat(stackbar): make label configurable
This commit introduces a new stackbar label configuration option backed
by the StackbarLabel enum, which now has two variants, Process and
Title.

The state tracker for this option is kept in an AtomicCell, and the
state tracker for StackbarMode has also been changed from an
Arc<Mutex<T>> to an AtomicCell to match.

resolve #826
2024-05-16 14:17:54 -07:00
LGUG2Z
77fc3973b6 perf(wm): switch to daily rotating log files 2024-05-15 17:54:34 -07:00
r3st
29b1794409 feat(cli): add promote-window cmd
This commit adds the promote-window command, which allows the user to
promote the window in the specified OperationDirection from the
currently focused window to the largest tile on the workspace layout.
2024-05-15 14:56:31 -07:00
LGUG2Z
1420334c94 feat(cli): add cycle-move-workspace-to-monitor cmd
This commit adds a new cli command, cycle-move-workspace-to-monitor.

After the introduction of the monitor reconciliator module in
combination with display_index_preferences, this command should never
really be necessary, however it is worth having as a backup.

resolve #718
2024-05-15 14:06:35 -07:00
LGUG2Z
82aa2edf8f docs(wm): border renames and aliases, ahk example
This commit renames a number of border-related code refs, removing the
ActiveWindow prefix since these borders are no longer just for the
active window.

Aliases have been added to preserve backwards compat for existing
configs.

An example AHK configuration file has been added to the Common Workflows
section of the docs site.

A link to the docs site has been added to the output of komorebic start.

A note has been added recommending that users disable system animations
for the best experience in the Getting Started guide.
2024-05-15 11:52:13 -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
LGUG2Z
6fce630be5 fix(wm): restore stackback on monocle off
This commit ensures that if a container stack has a stackbar, when
toggling off monocle mode, the stackbar will be restored as expected.
This requires an additional retile which it would be nice to avoid in
the future.
2024-04-21 12:35:23 -07:00
LGUG2Z
dfd0d604aa feat(cli): add move-to-monitor-workspace command
This commit adds a move-to-monitor-workspace command, which, following
the existing convention, does the same action as
send-to-monitor-workspace, but sets the focused monitor, workspace and
container to the window container once it is inserted into the target
monitor and workspace indices.
2024-04-20 14:58:57 -07:00
LGUG2Z
aff1081ccd feat(cli): detailed start failure error feedback
This commit ensures that after 3 failures to start komorebi with
'komorebic start', 'komorebi.exe' will be run directly in order to show
the detailed error output to the end user.
2024-04-20 14:37:27 -07:00
LGUG2Z
793e81d43d fix(cli): use force-quit if stop signal fails
This commit ensures that the komorebic stop command will force-quit
komorebi if the Stop SocketMessage handler fails.
2024-04-20 14:37:23 -07:00
LGUG2Z
eac4c8e9b1 fix(wm): add regex ws rule matching support
This commit ensures that enforce_workspace_rules() will try to match
exact exe names, classes and titles, and then against any potential
regexes that may have been used as keys to index matching rules against
the current window's exe, title and class.

Support for other matchers isn't implemented yet. Not sure it's worth
adding while using a HashMap to store the workspace rules, probably need
to rethink the data structure first.
2024-04-18 19:44:08 -07:00
جاد
6b42587af4 docs(ahk): add #requires line, remove asc ref
* re-add the #requires line that was removed in commit e044a5a
* remove the "generated by komorebic" comment which is incorrect. komorebic only generates an AHK v1 lib
* update common-workflows/autohotkey.md to remove line about `komorebi.generated.ahk file which no longer exists
2024-04-16 08:04:15 -07:00
LGUG2Z
46152621c0 feat(wm): immediate stackbar mode updates via ipc
This commit ensures that when the stackbar mode is updated via a
SocketMessage or static config update, any visible stackbars will have
their mode updated immediately without having to wait for user
interaction.
2024-04-16 07:54:31 -07:00
mmikeww
b78693118b Update autohotkey.md
komorebi.generated.ahk is no longer in the repo, it was deleted in commit 52122c401d, so remove the instruction to download it
2024-04-15 21:29:31 -04:00
mmikeww
7caa839a00 Update komorebic.lib.ahk
re-add the #Requires v2 line
remove the "generated by komorebic" comment
2024-04-15 21:24:11 -04:00
LGUG2Z
1671f31e3e feat(wm): add window debugging
This commit adds support for debugging windows and emitting information
about how they go through komorebi's decision making pipeline and rules
engines which ultimately decide how they are or aren't managed.
2024-04-15 15:05:20 -07:00
LGUG2Z
6fe46610fc refactor(wm): ensure configurability via ipc
This commit ensures that new features such as stackbar, particularly
where the configuration is located in the global state, can be
configured via SocketMessages.

A few structs had to be moved to komorebi-core to make this possible.
I've also cleaned up a bunch of strum snake_case attrs which seemed to
be unused.

A new GlobalState SocketMessage has been introduced, and going forward
we should make sure that this can send all global state to a requester,
and move global state out of the State handler, which should only handle
window manager state.
2024-04-15 08:36:28 -07:00
LGUG2Z
4ba3125dde fix(wm): ensure window restore on stackbar click
This commit ensures that a window is restored before a focus call is
made on it when switching windows in a stack by using a window's tab in
the stackbar.

This handles the issue where two clicks are required to trigger a layout
update the first time a window that is not currently at the top of the
stack is brought to the front via a stackbar click.
2024-04-13 22:38:41 -07:00
James Tucker
efa562de5c fix(wm): avoid out of range subtraction
This fixes the panic, but not the underlying problem.

Updates #757
Updates #758
2024-04-13 22:17:57 -07:00
James Tucker
b476bee1d8 fix(wm): fix focus changes with stackbar enabled
Notify all stackbars on focus change, and they now respond to changes,
but do not create focus changes themselves just from an update.
2024-04-13 22:07:53 -07:00
James Tucker
86b07f28dd chore(deps): update deps, including miow
Fixes #696
2024-04-13 21:59:48 -07:00
James Tucker
311e37c8a2 fix(wm): reduce errors from non-window events
As we have been working down some bugs from earlier changes, we
introduced some additional error conditions in the logs. Now that the
new focus approach is available, switching the stackbar to that means we
can avoid needing to pass down ForceUpdate and FocusChange events for
non-windows, which removes many of these cases.

In addition we do a check in should_manage that the target object is
actually a window, ignoring the event if it is not.
2024-04-13 21:14:59 -07:00
James Tucker
15c3b32608 feat(wm): report full errors in debug mode
This gives us a stack trace style error report in the log when in debug
mode, which makes it much quicker to track down the origin of an error.
2024-04-13 17:28:49 -07:00
James Tucker
28b46c54da fix(wm): correct use of z-order flags
We had been setting managed windows to HWND_TOPMOST which is a sticky
and viral parameter. This was also the cause of the border window ending
up behind other windows in an undesirable fashion, as even though it was
marked WS_EX_TOPMOST, we were then having to mark it HWND_NOTOPTMOST
when raising it to avoid it ending up drawing over other windows.

Since we've fixed the border window to no longer be visible when
unmanaged windows are focused, we can now set the border window to
HWND_TOP when we reposition, which will ensure it's drawing in the order
that we want.

Now we also set managed windows only to HWND_TOP, rather than
HWND_TOPMOST which stops us from incorrectly reordering internal
concerns vs. child windows and owned windows that we're not managing.
Windows are still brought to the foreground as expected/desired, but
they're no longer 'sticking' there, nor are they drawing over the border
window.

This change does have a slight transition behavior as it initially rolls
out, as prior versions of the Komorebi have been setting HWND_TOPMOST,
which as a sticky parameter won't be cleared until the application or
host system removes that flag. This means that the final z-order
behavior will come good eventually.

To immediately see the correct results, restarting affected apps or
logging out / in will do. Unfortuantely we can't just set
HWND_NOTTOPMOST, as similarly to setting HWND_TOPMOST, this can cause
issues with an applications intended owned-window Z-Ordering - mostly
affecting toolwindows and child windows, such as file dialogs, toolbars
and so on, most of which we do not manage.
2024-04-13 17:28:45 -07:00
James Tucker
732aca77b5 refactor(wm): use a new method to focus windows
Use the same method as FancyZones to enable setting the foreground
window. This makes it possible then to remove the thread attachment
behaviors that have a number of other complex side effects, and aren't
always allowed.

In addition, cleanup old focus/raise methods some, in particular the
border window is now explicitly not activated when it is raised, as it
should never be activated.
2024-04-13 16:51:01 -07:00
James Tucker
f56fc36557 refactor(wm): remove mutability from window and events
We had a mut requirement on some of the Window functions which may have
been vestigial or in preparation for more state on Window objects, but
presently unused. I removed that, as the Window struct is currently just
carrying an HWND value that's essentially always immutable - there's no
advantage to ever reusing a Window struct vs. making a new one for
another HWND.

In doing so we then no longer needed to be passing in mutable events, so
I applied a little simplification of the event receiver / dispatcher to
process_event. After that it became obvious that we could just pass the
owned event directly into process_event instead, which substantially
simplifies the ownership model and lifetime for those objects.

This is small, and shouldn't create any meaningful behavioral change.
2024-04-13 16:50:26 -07:00
LGUG2Z
5334e1944e fix(wm): ensure stackbar tab clicks trigger
This commit fixes a small regression that prevented events emitted from
clicks on stackbar tabs from being handled.
2024-04-13 12:02:46 -07:00
LGUG2Z
d8d087e621 fix(wm): ensure borders are drawn w/ stackbar
This commit fixes a small regression and ensures that the active window
border, when enabled, will be drawn as expected when a container stack
has a stackbar active.
2024-04-13 11:02:14 -07:00
James Tucker
b61146ead4 fix(wm): hide border when unmanged windows are focused
Previously we were dropping events that don't pertain to managed
windows, with one exception in should_manage that could probably do with
further cleanup (DisplayChange).

This first step fixes the latent border window problem, where we would
retain a border window when the last managed window was closed and focus
transitioned to an unmanaged window.
2024-04-13 10:22:03 -07:00
LGUG2Z
16cb811aa9 docs(license): switch to polyform, add contributing.md 2024-04-12 14:46:36 -07:00
LGUG2Z
862219b9a3 chore(dev): begin v0.1.25-dev 2024-04-11 16:59:01 -07:00
LGUG2Z
0fc75afb00 chore(release): v0.1.24 2024-04-09 17:57:41 -07:00
LGUG2Z
d67f355a17 refactor(logs): reduce noise on info level 2024-04-09 17:34:14 -07:00
LGUG2Z
3d0ed4cfc4 fix(wm): address regression from 5e714ca
This commit fixes a regression introduced in 5e714ca which broke the
cross-monitor mouse move behaviour.
2024-04-09 16:30:58 -07:00
Carlos Regis
21be01b9aa docs(mkdocs): show default loc for whkdrc (#739) 2024-04-08 16:48:52 -07:00
LGUG2Z
648ba672e3 docs(config): update schema version in docstring 2024-04-07 14:26:27 -07:00
LGUG2Z
af6529851e chore(dev): begin v0.1.24-dev 2024-04-07 14:18:29 -07:00
LGUG2Z
bea3d1fcbc chore(release): v0.1.23 2024-04-07 13:34:18 -07:00
LGUG2Z
e40fa11b0d docs(mkdocs): offline install, stackbar, border style 2024-04-05 18:37:16 -07:00
LGUG2Z
251ec3d53e fix(wm): address toggle-float container edge case
This commit ensures that the focused container index on a workspace is
updated appropriately if the user calls toggle-float on the last
Container in a Ring. Previously, doing this would leave a ghost tile.
Now that we check if the focused_idx is still valid after removing an
empty Container from the workspace, users will no longer be left with a
ghost tile.
2024-04-05 17:12:14 -07:00
LGUG2Z
5e714cafab fix(wm): limit resize/move path to managed hwnds
This commit ensures that the MoveResizeEnd handler will only proceed if
the window triggering the event is a managed window on the focused
workspace.

fix #733
2024-04-05 15:19:32 -07:00
LGUG2Z
6aa9be1ea0 fix(config): gracefully handled deprecated asc opt
This commit ensures that if an applications.yaml revision is passed
which includes the now-deprecated border_overflow option, komorebi will
gracefully handle it instead of crashing on an unknown enum variant
error.
2024-04-05 15:15:07 -07:00
dependabot[bot]
6238d1f848 chore(deps): bump h2 from 0.4.3 to 0.4.4
Bumps [h2](https://github.com/hyperium/h2) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.4.3...v0.4.4)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 08:37:53 -07:00
LGUG2Z
d83dc48230 feat(docs): deprecate border overflow identifiers 2024-04-04 14:45:23 -07:00
LGUG2Z
f2c4dadcde feat(docs): update schema.json + mkdocs cli pages 2024-04-04 14:34:58 -07:00
LGUG2Z
c05e9ea089 fix(wm): always check/ensure datadir creation
A user reported in an issue that their komorebi data dir did not persist
across reboots. While there have not been any other reports of this type
of system behaviour, it's worth just doing a mkdir -p equivalent call
whenever komorebi.exe is starting to ensure that the socket files can
always be created no matter what.

re #731
2024-04-04 12:56:55 -07:00
LGUG2Z
37aa99a537 feat(config): add active window border style opt
This commit adds a new active window border style configuration option
to komorebi.json, allowing users to explicitly opt in to square or
rounded active window borders to match whatever patches they may have
made at the system / dwm.exe level.
2024-04-02 19:39:19 -07:00
dependabot[bot]
dca32bead5 chore(deps): bump serde_json from 1.0.114 to 1.0.115
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.114 to 1.0.115.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.114...v1.0.115)

---
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-01 12:10:17 -07:00
dependabot[bot]
77eaddca1e chore(deps): bump reqwest from 0.11.27 to 0.12.2
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.27 to 0.12.2.
- [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.11.27...v0.12.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 12:10:08 -07:00
LGUG2Z
79fb098d04 docs(cli): add support + community links to start cmd 2024-03-25 17:55:28 -07:00
dependabot[bot]
d0bab4280a chore(deps): bump heck from 0.4.1 to 0.5.0
Bumps [heck](https://github.com/withoutboats/heck) from 0.4.1 to 0.5.0.
- [Changelog](https://github.com/withoutboats/heck/blob/master/CHANGELOG.md)
- [Commits](https://github.com/withoutboats/heck/commits)

---
updated-dependencies:
- dependency-name: heck
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 17:27:02 -07:00
dependabot[bot]
e616004da7 chore(deps): bump serde_yaml from 0.9.33 to 0.9.34+deprecated
Bumps [serde_yaml](https://github.com/dtolnay/serde-yaml) from 0.9.33 to 0.9.34+deprecated.
- [Release notes](https://github.com/dtolnay/serde-yaml/releases)
- [Commits](https://github.com/dtolnay/serde-yaml/compare/0.9.33...0.9.34)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 17:26:43 -07:00
LGUG2Z
c65060fbd9 feat(wm): add stackbar for multi-window containers
This commit introduces the stackbar feature through careful extracting
and refactoring of code from the Komorebi-UI hard-fork.

Unfortunately on the fork, this feature was not implemented using atomic
commits, which resulted in the implementation here being more of a
"reinterpretation" than a lift-and-shit of the referenced code.

Nevertheless, this commit represents a working version of the stackbar
feature.

resolve #681
2024-03-25 14:30:30 -07:00
dependabot[bot]
50a851a660 chore(deps): bump which from 6.0.0 to 6.0.1
Bumps [which](https://github.com/harryfei/which-rs) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/harryfei/which-rs/releases)
- [Changelog](https://github.com/harryfei/which-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harryfei/which-rs/compare/6.0.0...6.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 11:20:18 -07:00
dependabot[bot]
8ad6f4be1f chore(deps): bump regex from 1.10.3 to 1.10.4
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.3 to 1.10.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.3...1.10.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 11:19:46 -07:00
LGUG2Z
24ac0c4cd3 refactor(cli): avoid http reqs for sample configs
re #716
2024-03-24 21:07:10 -07:00
LGUG2Z
e0652d953a feat(config): add negative matching strategies
This commit adds support for negative matching strategies to complement
Equals, StartsWith, EndsWith and Contains.
2024-03-22 21:06:24 -07:00
LGUG2Z
d6e0ecc507 feat(config): add support for composite rules
This commit adds support for composite rules in the static configuration
file, and for float_identifiers in the applications.yaml file.

A new enum, MatchingRule provides two variants, Simple and Composite,
and is now used in place of IdWithIdentifier throughout komorebi.

In order to keep this new enum lean, a variant for
IdWithIdentifierAndComment has not been added, and references to it in
the old config generation code have been replaced with MatchingRule.

resolve #462, resolve #715, resolve #237
2024-03-22 17:51:20 -07:00
LGUG2Z
ca22cdb07f fix(wm): add hack for new firefox windows
This commit adds a 10 millisecond thread sleep specifically for Firefox
as an interim solution for the race condition which results in new
windows often not being tiled correctly until interacted with.

The idea to add a sleep instead of spamming the output with dbg! calls
comes from @kornel@mastodon.social:

https://mastodon.social/@kornel/112125851048707993
2024-03-19 20:27:25 -07:00
dependabot[bot]
dc38eae2af chore(deps): bump serde_yaml from 0.9.32 to 0.9.33
Bumps [serde_yaml](https://github.com/dtolnay/serde-yaml) from 0.9.32 to 0.9.33.
- [Release notes](https://github.com/dtolnay/serde-yaml/releases)
- [Commits](https://github.com/dtolnay/serde-yaml/compare/0.9.32...0.9.33)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 13:24:46 -07:00
dependabot[bot]
66446f571c chore(deps): bump os_info from 3.8.0 to 3.8.1
Bumps [os_info](https://github.com/stanislav-tkach/os_info) from 3.8.0 to 3.8.1.
- [Release notes](https://github.com/stanislav-tkach/os_info/releases)
- [Changelog](https://github.com/stanislav-tkach/os_info/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stanislav-tkach/os_info/compare/v3.8.0...v3.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 13:18:37 -07:00
LGUG2Z
b2f6329963 fix(wm): raise applicationframehost apps via state
This commit ensures that ApplicationFrameHost.exe applications developed
by Microsoft are raised correctly when a user has the custom komorebi
ffm implementation enabled.

The Win32 API calls EnumWindows and WindowFromPoint return different
HWNDs for the same windows because of some jank in how the
ApplicationFrameHost apps are developed.

To avoid this inconsistency on the Win32 API level, komorebi now queries
its own state when looking up HWNDs for windows at any given cursor
position.
2024-03-16 15:35:37 -07:00
LGUG2Z
bc46f65f64 fix(wm): use focus fn in komorebi ffm
This commit swaps out the old "raise" fn for the more up-to-date and
tested "focus" fn when raising a window for focus when the "komorebi"
implementation of focus follows mouse is enabled.
2024-03-15 18:57:38 -07:00
LGUG2Z
69573c383f refactor(wm): use notopmost flag instead of bottom
It seems like when we use the bottom flag, Rainmeter widgets will be
drawn on top of windows that are managed by komorebi. After looking at
the GlazeWM codebase, where this issue does not occur, it seems like the
difference is made by the use of the notopmost flag.

resolve #679
2024-03-15 18:48:53 -07:00
LGUG2Z
45a3f2a6b5 chore(dev): begin v0.1.23-dev 2024-03-15 18:48:28 -07:00
dependabot[bot]
5af00b64cf chore(deps): bump softprops/action-gh-release from 1 to 2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:27:03 -07:00
dependabot[bot]
81dff3279c chore(deps): bump clap from 4.5.1 to 4.5.2
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.1...v4.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:26:14 -07:00
dependabot[bot]
2f17e4bb29 chore(deps): bump reqwest from 0.11.24 to 0.11.25
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.24 to 0.11.25.
- [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.11.24...v0.11.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:24:32 -07:00
dependabot[bot]
1b966d3731 chore(deps): bump ctrlc from 3.4.2 to 3.4.4
Bumps [ctrlc](https://github.com/Detegr/rust-ctrlc) from 3.4.2 to 3.4.4.
- [Release notes](https://github.com/Detegr/rust-ctrlc/releases)
- [Commits](https://github.com/Detegr/rust-ctrlc/compare/3.4.2...3.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:24:14 -07:00
dependabot[bot]
e58d776f81 chore(deps): bump sysinfo from 0.30.6 to 0.30.7
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.6 to 0.30.7.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.30.6...v0.30.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 11:24:00 -07:00
LGUG2Z
40e77fddfe chore(release): v0.1.22 2024-03-03 21:00:37 -08:00
LGUG2Z
9c8a50fe80 docs(mkdocs): updates for v0.1.22 2024-03-03 20:53:25 -08:00
LGUG2Z
41e9068fca chore(wm): add debug event info to ignored windows 2024-03-03 14:58:12 -08:00
LGUG2Z
3690f8ebd8 ci(github): add common cargo checks 2024-03-03 13:31:20 -08:00
LGUG2Z
7b24474ef2 fix(wm): prevent ghost active border on empty ws
This commit ensures that a "ghost border" will not be drawn when
switching to empty workspaces if active_window_border = true.
2024-03-02 19:30:50 -08:00
LGUG2Z
f675717844 ci(github): add goreleaser check to build 2024-03-02 15:23:06 -08:00
LGUG2Z
38b0418c3b fix(subscriptions): emit ws event on empty targets
This commit ensures that workspace change events get emitted even when
changing to workspaces with no window containers. Previously these were
failing due to early returns triggered when the workspace focused did
not have a focused container.
2024-03-02 11:15:20 -08:00
LGUG2Z
6781f34930 fix(wm): restore full mouse resize functionality
This commit fixes a regression that was most likely introduced in #678
which changed a bunch of stuff related to window and border dimension
calculation.

See commit 4e98d7d36d for more information
as the same approach to fix the behaviour there has been applied here.
2024-03-01 19:07:05 -08:00
Javier Portillo
4919872e1a fix(wm): adds special case for grid stacks to the right 2024-03-01 16:14:42 -08:00
LGUG2Z
d730c3c72d feat(config): support parsing json w/ comments
This commit switches out the serde_json crate with the
serde_json_lenient crate, forked by Google, which allows for JSON files
with comments to be parsed properly.

Users can set the format of komorebi.json to "jsonc" in their editors in
order to write // comments without being faced with lint errors.

The expected file extension remains the same (json). komorebi and
komorebic will not look for files with the "jsonc" file extension or any
other JSON-variant file extension.

resolve #693
2024-02-29 17:35:47 -08:00
LGUG2Z
4e98d7d36d fix(wm): restore drag-to-swap window functionality
This commit fixes a regression that was most likely introduced in #678
which changed a bunch of stuff related to window and border dimension
calculation.

While we could previously assume that the points resize.right and
resize.bottom would always be 0 if we were dealing with a move rather
than a resize, these two points now depend on the values of BORDER_WIDTH
and BORDER_OFFSET. The code has been updated to reflect this and
calculate this constant just-in-time.
2024-02-29 17:22:56 -08:00
eythaann
2bceff4edc feat(wm): add path variant to application identifiers
This commit add a new way to match applications
through its paths, allowing to match a bunch
of applications that share a parent folder.
2024-02-28 12:04:46 -08:00
dependabot[bot]
b32bce8713 chore(deps): bump miette from 5.10.0 to 7.1.0
Bumps [miette](https://github.com/zkat/miette) from 5.10.0 to 7.1.0.
- [Release notes](https://github.com/zkat/miette/releases)
- [Changelog](https://github.com/zkat/miette/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zkat/miette/compare/miette-derive-v5.10.0...miette-derive-v7.1.0)

---
updated-dependencies:
- dependency-name: miette
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-27 22:51:28 -08:00
dependabot[bot]
47af40cf9e chore(deps): bump hotwatch from 0.4.6 to 0.5.0
Bumps [hotwatch](https://github.com/francesca64/hotwatch) from 0.4.6 to 0.5.0.
- [Release notes](https://github.com/francesca64/hotwatch/releases)
- [Changelog](https://github.com/francesca64/hotwatch/blob/main/CHANGELOG.md)
- [Commits](https://github.com/francesca64/hotwatch/compare/v0.4.6...v0.5.0)

---
updated-dependencies:
- dependency-name: hotwatch
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-27 21:55:12 -08:00
LGUG2Z
2b9fbc2074 chore(deps): bump windows-rs from 0.52 to 0.54 2024-02-27 18:33:29 -08:00
LGUG2Z
1a8b6a7398 feat(client): introduce komorebi-client crate
This commit introduces the komorebi-client library crate for other Rust
applications to interact with a running instance of komorebi over Unix
Domain Sockets.

Currently the crate re-exports everything one might find in the
komorebi::State struct and everything that is publicly exposed in
komorebi-core.

Public types and methods are still lacking documentation, and this crate
should not be published on crates.io until this is no longer the case.
2024-02-26 18:18:40 -08:00
115 changed files with 11066 additions and 3461 deletions

View File

@@ -73,6 +73,11 @@ jobs:
- name: Install the target
run: |
rustup target install ${{ matrix.target }}
- name: Run Cargo checks
run: |
cargo fmt --check
cargo check
cargo clippy
- name: Run a full build
run: |
cargo build --locked --release --target ${{ matrix.target }}
@@ -88,11 +93,17 @@ jobs:
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebic-no-console.exe
target/${{ matrix.target }}/release/komorebi-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
target/${{ matrix.target }}/release/komorebic-no-console.pdb
target/${{ matrix.target }}/release/komorebi-gui.pdb
target/wix/komorebi-*.msi
retention-days: 7
- name: Check GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: build --skip=validate --clean
# Release
- name: Generate changelog
@@ -106,12 +117,12 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v')
with:
version: latest
args: release --skip-validate --clean --release-notes=CHANGELOG.md
args: release --skip=validate --clean --release-notes=CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
- name: Add MSI to release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: "target/wix/komorebi-*.msi"

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@
/target
CHANGELOG.md
dummy.go
komorebi.ahk
komorebi.ahk
komorebic/applications.yaml

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,19 +28,28 @@ 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:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
- id: komorebi-gui
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-gui
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
format: zip
files:
- LICENSE
- LICENSE.md
- CHANGELOG.md
checksum:

45
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,45 @@
# Contributing to the Project
The project is a collection of contributions from both the project leaders and
community members. There are many ways to contribute, this can include content
in the project repositories, as well as contributing in public and private
conversation, assisting users, writing blog posts, and many other ways.
## How contributions are made
Contributions to the project primarily happen in the project source
repositories, but may also occur in other places, such as discussion forums and
public and private discourse.
## Contributing content to the Project
In order for the project leaders to manage sustained progress toward the
project goals and maintain project velocity, focus and quality, the project may
adjust the license terms over time.
Content contributed to the project must therefore be provided under
sufficiently liberal terms to allow these operations to proceed unimpeded. As
such contributions are accepted with the following understanding:
* Contributed content is licensed under the terms of the 0-BSD license
* Contributors accept the terms of the project license at the time of
contribution
By making a contribution, you accept both the current project license terms,
and that all contributions that you have made are provided under the terms of
the 0-BSD license.
## Zero-Clause BSD
```
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
```

3970
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,20 +4,26 @@ resolver = "2"
members = [
"derive-ahk",
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
]
[workspace.dependencies]
windows-interface = { version = "0.52" }
windows-implement = { version = "0.52" }
windows-interface = { version = "0.53" }
windows-implement = { version = "0.53" }
dunce = "1"
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.52"
version = "0.54"
features = [
"implement",
"Win32_System_Com",

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Jade Iqbal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

105
LICENSE.md Normal file
View File

@@ -0,0 +1,105 @@
# PolyForm Strict License 1.0.0
<https://polyformproject.org/licenses/strict/1.0.0>
## Acceptance
In order to get any license under these terms, you must agree
to them as both strict obligations and conditions to all
your licenses.
## Copyright License
The licensor grants you a copyright license for the software
to do everything you might do with the software that would
otherwise infringe the licensor's copyright in it for any
permitted purpose, other than distributing the software or
making changes or new works based on the software.
## Patent License
The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for
the benefit of public knowledge, personal study, private
entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
## Fair Use
You may have "fair use" rights for the software under the
law. These terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of
your licenses to anyone else, or prevent the licensor from
granting licenses to anyone else. These terms do not imply
any other licenses.
## Patent Defense
If you make any written claim that the software infringes or
contributes to infringement of any patent, your patent license
for the software granted under these terms ends immediately. If
your company makes such a claim, your patent license ends
immediately for work on behalf of your company.
## Violations
The first time you are notified in writing that you have
violated any of these terms, or done anything with the software
not covered by your licenses, your licenses can nonetheless
continue if you come into full compliance with these terms,
and take practical steps to correct past violations, within
32 days of receiving notice. Otherwise, all your licenses
end immediately.
## No Liability
***As far as the law allows, the software comes as is, without
any warranty or condition, and the licensor will not be liable
to you for any damages arising out of these terms or the use
or nature of the software, under any kind of legal claim.***
## Definitions
The **licensor** is the individual or entity offering these
terms, and the **software** is the software the licensor makes
available under these terms.
**You** refers to the individual or entity agreeing to these
terms.
**Your company** is any legal entity, sole proprietorship,
or other kind of organization that you work for, plus all
organizations that have control over, are under the control of,
or are under common control with that organization. **Control**
means ownership of substantially all the assets of an entity,
or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the
software under these terms.
**Use** means anything you do with the software requiring one
of your licenses.

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
@@ -129,7 +142,7 @@ if you want to test them together or create a build with everything integrated.
## Refactors to the codebase must have prior approval
`komorebi` is mature codebase with an internal consistency and structure that has developed organically over close to
`komorebi` is a mature codebase with an internal consistency and structure that has developed organically over close to
half a decade.
There are [countless hours of live coding videos](https://youtube.com/@LGUG2Z) demonstrating work on this project and
@@ -138,8 +151,8 @@ distinguishing monitors by manufacturer hardware identifiers and video card port
Refactors to the structure of the codebase are not taken lightly and require prior discussion and approval.
Please do not start refactoring the codebase with the expectation of having your changes integrated into the codebase
until you receive an explicit approval or a request to do so.
Please do not start refactoring the codebase with the expectation of having your changes integrated until you receive an
explicit approval or a request to do so.
Similarly, when implementing features and bug fixes, please stick to the structure of the codebase as much as possible
and do not take this as an opportunity to do some "refactoring along the way".
@@ -169,6 +182,20 @@ ability for users to specify colours in `komorebi.json` in Hex format alongside
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
required.
## License
`komorebi` is licensed under the [PolyForm Strict 1.0.0
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
this means that you are free to do whatever you want with `komorebi` other than
redistribution, or distribution of new works (ie. hard-forks) based on the
software.
Anyone is free to make their own fork of `komorebi` with changes intended
either for personal use or for integration back upstream via pull requests.
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
code contributions to `komorebi` are licensed.
# Development
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
@@ -324,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.22"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.25"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -1,26 +0,0 @@
# active-window-border-colour
```
Set the colour for the active window border
Usage: komorebic.exe active-window-border-colour [OPTIONS] <R> <G> <B>
Arguments:
<R>
Red
<G>
Green
<B>
Blue
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle]
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# active-window-border-offset
```
Set the offset for the active window border
Usage: komorebic.exe active-window-border-offset <OFFSET>
Arguments:
<OFFSET>
Desired offset of the active window border
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# active-window-border-width
```
Set the width for the active window border
Usage: komorebic.exe active-window-border-width <WIDTH>
Arguments:
<WIDTH>
Desired width of the active window border
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# alt-focus-hack
```
Enable or disable a hack simulating ALT key presses to ensure focus changes succeed
Usage: komorebic.exe alt-focus-hack <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-h, --help
Print help
```

26
docs/cli/border-colour.md Normal file
View File

@@ -0,0 +1,26 @@
# border-colour
```
Set the colour for a window border kind
Usage: komorebic.exe border-colour [OPTIONS] <R> <G> <B>
Arguments:
<R>
Red
<G>
Green
<B>
Blue
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle, unfocused]
-h, --help
Print help
```

16
docs/cli/border-offset.md Normal file
View File

@@ -0,0 +1,16 @@
# border-offset
```
Set the border offset
Usage: komorebic.exe border-offset <OFFSET>
Arguments:
<OFFSET>
Desired offset of the window border
Options:
-h, --help
Print help
```

16
docs/cli/border-width.md Normal file
View File

@@ -0,0 +1,16 @@
# border-width
```
Set the border width
Usage: komorebic.exe border-width <WIDTH>
Arguments:
<WIDTH>
Desired width of the window border
Options:
-h, --help
Print help
```

View File

@@ -1,9 +1,9 @@
# active-window-border
# border
```
Enable or disable the active window border
Enable or disable borders
Usage: komorebic.exe active-window-border <BOOLEAN_STATE>
Usage: komorebic.exe border <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>

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]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -1,7 +1,7 @@
# check
```
Output various important komorebi-related environment values
Check komorebi configuration and related files for common errors
Usage: komorebic.exe check

12
docs/cli/configuration.md Normal file
View File

@@ -0,0 +1,12 @@
# configuration
```
Show the path to komorebi.json
Usage: komorebic.exe configuration
Options:
-h, --help
Print help
```

View File

@@ -10,6 +10,7 @@ Arguments:
Possible values:
- swap: Swap the window container with the window container at the edge of the adjacent monitor
- insert: Insert the window container into the focused workspace on the adjacent monitor
- no-op: Do nothing if trying to move a window container in the direction of an adjacent monitor
Options:
-h, --help

View File

@@ -0,0 +1,16 @@
# cycle-move-workspace-to-monitor
```
Move the focused workspace monitor in the given cycle direction
Usage: komorebic.exe cycle-move-workspace-to-monitor <CYCLE_DIRECTION>
Arguments:
<CYCLE_DIRECTION>
[possible values: previous, next]
Options:
-h, --help
Print help
```

View File

@@ -1,10 +0,0 @@
# docgen
```
Usage: komorebic.exe docgen
Options:
-h, --help
Print help
```

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe float-rule <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

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

12
docs/cli/gui.md Normal file
View File

@@ -0,0 +1,12 @@
# gui
```
Launch the komorebi-gui debugging tool
Usage: komorebic.exe gui
Options:
-h, --help
Print help
```

View File

@@ -1,19 +0,0 @@
# identify-border-overflow-application
```
Identify an application that has overflowing borders
Usage: komorebic.exe identify-border-overflow-application <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
<ID>
Identifier as a string
Options:
-h, --help
Print help
```

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe identify-layered-application <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe identify-object-name-change-application <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe identify-tray-application <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe initial-named-workspace-rule <IDENTIFIER> <ID> <WORKSPACE>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe initial-workspace-rule <IDENTIFIER> <ID> <MONITOR> <WORKSPA
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe manage-rule <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

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]
[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]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe named-workspace-rule <IDENTIFIER> <ID> <WORKSPACE>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -0,0 +1,16 @@
# promote-window
```
Promote the window in the specified direction
Usage: komorebic.exe promote-window <OPERATION_DIRECTION>
Arguments:
<OPERATION_DIRECTION>
[possible values: left, right, up, down]
Options:
-h, --help
Print help
```

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe remove-title-bar <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -1,9 +1,9 @@
# subscribe
# subscribe-pipe
```
Subscribe to komorebi events
Subscribe to komorebi events using a Named Pipe
Usage: komorebic.exe subscribe <NAMED_PIPE>
Usage: komorebic.exe subscribe-pipe <NAMED_PIPE>
Arguments:
<NAMED_PIPE>

View File

@@ -0,0 +1,16 @@
# subscribe-socket
```
Subscribe to komorebi events using a Unix Domain Socket
Usage: komorebic.exe subscribe-socket <SOCKET>
Arguments:
<SOCKET>
Name of the socket to send event notifications to
Options:
-h, --help
Print help
```

View File

@@ -1,9 +1,9 @@
# unsubscribe
# unsubscribe-pipe
```
Unsubscribe from komorebi events
Usage: komorebic.exe unsubscribe <NAMED_PIPE>
Usage: komorebic.exe unsubscribe-pipe <NAMED_PIPE>
Arguments:
<NAMED_PIPE>

View File

@@ -0,0 +1,16 @@
# unsubscribe-socket
```
Unsubscribe from komorebi events
Usage: komorebic.exe unsubscribe-socket <SOCKET>
Arguments:
<SOCKET>
Name of the socket to stop sending event notifications to
Options:
-h, --help
Print help
```

12
docs/cli/whkdrc.md Normal file
View File

@@ -0,0 +1,12 @@
# whkdrc
```
Show the path to whkdrc
Usage: komorebic.exe whkdrc
Options:
-h, --help
Print 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]
[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]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe workspace-rule <IDENTIFIER> <ID> <MONITOR> <WORKSPACE>
Arguments:
<IDENTIFIER>
[possible values: exe, class, title]
[possible values: exe, class, title, path]
<ID>
Identifier as a string

View File

@@ -1,38 +0,0 @@
# Active Window Border
If you would like to add a visual border around the currently focused window,
ensure the following options are defined in the `komorebi.json` configuration
file.
```json
{
"active_window_border": true,
"active_window_border_colours": {
"single": {
"r": 66,
"g": 165,
"b": 245
},
"stack": {
"r": 256,
"g": 165,
"b": 66
},
"monocle": {
"r": 255,
"g": 51,
"b": 153
}
}
}
```
It is important to note that the active window border will only apply to
windows managed by `komorebi`.
This feature is not considered stable and you may encounter visual artifacts
from time to time.
[![Watch the tutorial
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)

View File

@@ -1,7 +1,5 @@
# AutoHotKey
<!-- TODO: Update this completely -->
If you would like to use Autohotkey, please make sure you have AutoHotKey v2
installed.
@@ -9,22 +7,16 @@ Generally, users who opt for AHK will have specific needs that can only be
addressed by the advanced functionality of AHK, and so they are assumed to be
able to craft their own configuration files.
If you would like to try out AHK, a simple sample configuration powered by
`komorebic.lib.ahk` is provided as a starting point. This sample configuration
does not take into account the use of a static configuration file; if you
choose to use a static configuration file alongside AHK, you can remove all the
configuration options from your `komorebi.ahk` and use it solely to handle
hotkey bindings.
If you would like to try out AHK, here is a simple sample configuration which
largely matches the `whkdrc` sample configuration.
```powershell
# save the latest generated komorebic library to ~/komorebic.lib.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
# save the latest generated app-specific config tweaks and fixes to ~/komorebi.generated.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.generated.ahk -OutFile $Env:USERPROFILE\komorebi.generated.ahk
# save the sample komorebi configuration file to ~/komorebi.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
```
{% include "../komorebi.ahk" %}
```
By default, the `komorebi.ahk` file should be located in the `$Env:USERPROFILE`
directory, however, if `$Env:KOMOREBI_CONFIG_HOME` is set, it should be located
there.
Once the file is in place, you can stop komorebi and whkd by running `komorebic stop --whkd`,
and then start komorebi with Autohotkey by running `komorebic start --ahk`.

View File

@@ -0,0 +1,28 @@
# Borders
If you would like to add a visual border around both the currently focused window
and unfocused windows ensure the following options are defined in the `komorebi.json`
configuration file.
```json
{
"border": true,
"border_width": 8,
"border_offset": -1,
"border_style": "System",
"border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399",
"unfocused": "#808080"
}
}
```
It is important to note that borders will only apply to windows managed by `komorebi`.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.
[![Watch the tutorial
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)

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

@@ -2,13 +2,15 @@
If you would like to remove all gaps by default, both between windows
themselves, and between the monitor edges and the windows, you can set the
following two configuration options to `0` in the `komorebi.json` configuration
file.
following configuration options to `0` and `-1` in the `komorebi.json`
configuration file.
```json
{
"default_workspace_padding": 0,
"default_container_padding": 0
"default_container_padding": 0,
"border_width": 0,
"border_offset": -1
}
```

View File

@@ -0,0 +1,23 @@
# Stackbar
If you would like to add a visual stackbar to show which windows are in a container
stack ensure the following options are defined in the `komorebi.json` configuration
file.
```json
{
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300,
"focused_text": "#00a542",
"unfocused_text": "#b3b3b3",
"background": "#141414"
}
}
}
```
This feature is not considered stable, and you may encounter visual artifacts
from time to time.

View File

@@ -16,6 +16,12 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart
```
With the example configurations downloaded, you can now start `komorebi` and `whkd.
```powershell
komorebic start --whkd
```
## komorebi.json
The example window manager configuration sets some sane defaults and provides
@@ -95,6 +101,16 @@ monocle.
+-------+-----+
```
#### RightMainVerticalStack
```
+-----+-------+
| | |
+-----+ |
| | |
+-----+-------+
```
#### Horizontal Stack
```
@@ -116,6 +132,7 @@ monocle.
```
#### Rows
If you have a vertical monitor, I recommend using this layout.
```
@@ -127,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.
```
@@ -139,12 +157,26 @@ 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!
```
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
| | | | | | | | | | | | | | |
| | | | | | | | | | | | | +---+
+-----+-----+ | +---+---+ +---+---+---+ +---+---| |
| | | | | | | | | | | | | +---+
| | | | | | | | | | | | | | |
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
4 windows 5 windows 6 windows 7 windows
```
## whkdrc
`whkd` is a fairly basic piece of software with a simple configuration format:
key bindings go to the left of the, and shell commands go to the right of the
colon.
key bindings go to the left of the colon, and shell commands go to the right of the
colon. By default, the `whkdrc` file should be located in the `$Env:USERPROFILE/.config/` directory.
Please remember that `whkd` does not support overriding Microsoft's limitations
on hotkey bindings that include the `Windows` key. If this is important to you,
@@ -164,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

@@ -30,9 +30,10 @@ to manipulate the window manager, you use
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also built
it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
- [Scoop](#scoop)
- [WinGet](#winget)
- [Building from source](#building-from-source)
- [Scoop](#scoop)
- [WinGet](#winget)
- [Building from source](#building-from-source)
- [Offline](#offline)
## Long path support
@@ -44,6 +45,12 @@ running the following command in an Administrator Terminal before installing
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
```
## Disabling Unnecessary System Animations
It is highly recommended that you enable the "Turn off all unnecessary animations (when possible)" option in
"Control Panel > Ease of Access > Ease of Access Centre / Make the computer easier to see" for the best performance with
komorebi.
## Scoop
Make sure you have installed [`scoop`](https://scoop.sh) and verified that
@@ -107,7 +114,17 @@ Clone the git repository, enter the directory, and build the following binaries:
cargo +stable install --path komorebi --locked
cargo +stable install --path komorebic --locked
cargo +stable install --path komorebic-no-console --locked
cargo +stable install --path komorebi-gui --locked
```
If the binaries have been built and added to your `$PATH` correctly, you should
see some output when running `komorebi --help` and `komorebic --help`
### Offline
Download the latest [komorebi](https://github.com/LGUG2Z/komorebi/releases)
and [whkd](https://github.com/LGUG2Z/whkd/releases) MSI installers on an internet-connected computer, then copy them to
an offline machine to install.
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
first-time set up and running komorebi require an internet connection).

View File

@@ -1,24 +1,60 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/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,
"active_window_border": false,
"active_window_border_colours": {
"single": { "r": 66, "g": 165, "b": 245 },
"stack": { "r": 256, "g": 165, "b": 66 },
"monocle": { "r": 255, "g": 51, "b": 153 }
"border": true,
"border_width": 8,
"border_offset": -1,
"border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399",
"unfocused": "#808080"
},
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300,
"focused_text": "#00a542",
"unfocused_text": "#b3b3b3",
"background": "#141414"
}
},
"monitors": [
{
"workspaces": [
{ "name": "I", "layout": "BSP" },
{ "name": "II", "layout": "VerticalStack" },
{ "name": "III", "layout": "HorizontalStack" },
{ "name": "IV", "layout": "UltrawideVerticalStack" },
{ "name": "V", "layout": "Rows" }
{
"name": "I",
"layout": "BSP"
},
{
"name": "II",
"layout": "VerticalStack"
},
{
"name": "III",
"layout": "HorizontalStack"
},
{
"name": "IV",
"layout": "UltrawideVerticalStack"
},
{
"name": "V",
"layout": "Rows"
},
{
"name": "VI",
"layout": "Grid"
},
{
"name": "VII",
"layout": "RightMainVerticalStack"
}
]
}
]

56
docs/release/v0-1-22.md Normal file
View File

@@ -0,0 +1,56 @@
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
## tl;dr
The way windows are sized and drawn has been improved to remove the need to manually specify and remove invisible
borders for applications that overflow them. If you use the active window border, the first time you launch `v0.1.22`
you may end up with a _huge_ border due to these changes.
`active_window_border_width` and `active_window_border_offset` have been renamed to `border_width` and `border_offset`
as they now also apply outside the context of the active window border.
```json
{
"active_window_border": true,
"border_width": 8,
"border_offset": -1
}
```
Users of the active window border should start from these settings and read the notes below before making further
adjustments.
## Changes to `active_window_border`, and window sizing:
- The border no longer creates a second drop-shadow around the active window
- Windows are now sized to fill the layout region entirely, ignoring window decorations such as drop shadows
- Border offset now starts exactly at the paint edge of the window on all sides
- Windows are sized such that the border offset and border width are taken into account
## Recommended patterns
### Gapless
- Disable "transparency effects" Personalization > Colors
- Set the following settings in `komorebi.json`:
```json
{
"default_workspace_padding": 0,
"default_container_padding": 0,
"border_offset": -1,
"border_width": 0
}
```
### 1px border
A 1px border is drawn around the window edge. Users may see a gap for a single pixel, if the system theme has a
transparent edge - this is the windows themed edge, and is not present for all applications.
```json
{
"border_offset": 0,
"border_width": 1
}
```

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

@@ -10,6 +10,9 @@ alt + shift + o : komorebic reload-configuration
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
# alt + b : if ($wshell.AppActivate('Chrome') -eq $False) { start chrome }
alt + q : komorebic close
alt + m : komorebic minimize
# Focus windows
alt + h : komorebic focus left
alt + j : komorebic focus down
@@ -56,8 +59,18 @@ alt + y : komorebic flip-layout vertical
alt + 1 : komorebic focus-workspace 0
alt + 2 : komorebic focus-workspace 1
alt + 3 : komorebic focus-workspace 2
alt + 4 : komorebic focus-workspace 3
alt + 5 : komorebic focus-workspace 4
alt + 6 : komorebic focus-workspace 5
alt + 7 : komorebic focus-workspace 6
alt + 8 : komorebic focus-workspace 7
# Move windows across workspaces
alt + shift + 1 : komorebic move-to-workspace 0
alt + shift + 2 : komorebic move-to-workspace 1
alt + shift + 3 : komorebic move-to-workspace 2
alt + shift + 4 : komorebic move-to-workspace 3
alt + shift + 5 : komorebic move-to-workspace 4
alt + shift + 6 : komorebic move-to-workspace 5
alt + shift + 7 : komorebic move-to-workspace 6
alt + shift + 8 : komorebic move-to-workspace 7

View File

@@ -16,19 +16,12 @@ fmt:
install-target target:
cargo +stable install --path {{ target }} --locked
prepare:
komorebic ahk-asc '~/.config/komorebi/applications.yaml'
komorebic pwsh-asc '~/.config/komorebi/applications.yaml'
cat '~/.config/komorebi/komorebi.generated.ps1' >komorebi.generated.ps1
cat '~/.config/komorebi/komorebi.generated.ahk' >komorebi.generated.ahk
install:
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi
run:
just install-target komorebic
cargo +stable run --bin komorebi --locked
warn $RUST_LOG="warn":
@@ -44,17 +37,12 @@ trace $RUST_LOG="trace":
just run
deadlock $RUST_LOG="trace":
just install-komorebic
cargo +stable run --bin komorebi --locked --features deadlock_detection
docgen:
komorebic docgen
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
exampledocs:
cp whkdrc.sample docs/whkdrc.sample
cp komorebi.example.json docs/komorebi.example.json
schemagen:
komorebic static-config-schema > schema.json
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\

View File

@@ -0,0 +1,12 @@
[package]
name = "komorebi-client"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi = { path = "../komorebi" }
komorebi-core = { path = "../komorebi-core" }
uds_windows = "1"
serde_json = { workspace = true }

View File

@@ -0,0 +1,91 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::container::Container;
pub use komorebi::monitor::Monitor;
pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::BorderColours;
pub use komorebi::GlobalState;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::TabsConfig;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::BorderStyle;
pub use komorebi_core::CustomLayout;
pub use komorebi_core::CycleDirection;
pub use komorebi_core::DefaultLayout;
pub use komorebi_core::Direction;
pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
pub use komorebi_core::StackbarLabel;
pub use komorebi_core::StackbarMode;
pub use komorebi_core::WindowKind;
use komorebi::DATA_DIR;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::Shutdown;
pub use uds_windows::UnixListener;
use uds_windows::UnixStream;
const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
}
}
Ok(())
}
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
let mut reader = BufReader::new(stream);
let mut response = String::new();
reader.read_to_string(&mut response)?;
Ok(response)
}
pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
let socket = DATA_DIR.join(name);
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {}
_ => {
return Err(error);
}
},
};
let listener = UnixListener::bind(&socket)?;
send_message(&SocketMessage::AddSubscriberSocket(name.to_string()))?;
Ok(listener)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-core"
version = "0.1.22-dev.0"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -8,7 +8,7 @@ edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_json = { workspace = true }
serde_yaml = "0.9"
strum = { version = "0.26", features = ["derive"] }
schemars = "0.8"

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,
@@ -321,7 +606,6 @@ impl Arrangement for CustomLayout {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum Axis {
Horizontal,
Vertical,
@@ -370,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();
@@ -563,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];
@@ -653,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

@@ -8,15 +8,17 @@ use strum::EnumString;
use crate::ApplicationIdentifier;
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema)]
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ApplicationOptions {
ObjectNameChange,
Layered,
BorderOverflow,
TrayAndMultiWindow,
Force,
BorderOverflow,
}
impl ApplicationOptions {
@@ -29,15 +31,15 @@ impl ApplicationOptions {
ApplicationOptions::Layered => {
format!("komorebic.exe identify-layered-application {kind} \"{id}\"",)
}
ApplicationOptions::BorderOverflow => {
format!("komorebic.exe identify-border-overflow-application {kind} \"{id}\"",)
}
ApplicationOptions::TrayAndMultiWindow => {
format!("komorebic.exe identify-tray-application {kind} \"{id}\"",)
}
ApplicationOptions::Force => {
format!("komorebic.exe manage-rule {kind} \"{id}\"")
}
ApplicationOptions::BorderOverflow => {
unreachable!("deprecated");
}
}
}
@@ -50,6 +52,13 @@ impl ApplicationOptions {
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MatchingRule {
Simple(IdWithIdentifier),
Composite(Vec<IdWithIdentifier>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifier {
pub kind: ApplicationIdentifier,
@@ -66,6 +75,10 @@ pub enum MatchingStrategy {
EndsWith,
Contains,
Regex,
DoesNotEndWith,
DoesNotStartWith,
DoesNotEqual,
DoesNotContain,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -95,14 +108,14 @@ pub struct ApplicationConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<ApplicationOptions>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
pub float_identifiers: Option<Vec<MatchingRule>>,
}
impl ApplicationConfiguration {
pub fn populate_default_matching_strategies(&mut self) {
if self.identifier.matching_strategy.is_none() {
match self.identifier.kind {
ApplicationIdentifier::Exe => {
ApplicationIdentifier::Exe | ApplicationIdentifier::Path => {
self.identifier.matching_strategy = Option::from(MatchingStrategy::Equals);
}
ApplicationIdentifier::Class | ApplicationIdentifier::Title => {}
@@ -181,19 +194,21 @@ impl ApplicationConfigurationGenerator {
}
if let Some(float_identifiers) = app.float_identifiers {
for float in float_identifiers {
let float_rule =
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
for matching_rule in float_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule =
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if let Some(comment) = float.comment {
lines.push(format!("# {comment}"));
};
// if let Some(comment) = float.comment {
// lines.push(format!("# {comment}"));
// };
lines.push(float_rule);
lines.push(float_rule);
}
}
}
}
@@ -230,21 +245,23 @@ impl ApplicationConfigurationGenerator {
}
if let Some(float_identifiers) = app.float_identifiers {
for float in float_identifiers {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
float.kind, float.id
);
for matching_rule in float_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
float.kind, float.id
);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if let Some(comment) = float.comment {
lines.push(format!("; {comment}"));
};
// if let Some(comment) = float.comment {
// lines.push(format!("; {comment}"));
// };
lines.push(float_rule);
lines.push(float_rule);
}
}
}
}

View File

@@ -10,7 +10,6 @@ use strum::EnumString;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum CycleDirection {
Previous,
Next,

View File

@@ -10,9 +10,18 @@ use crate::Rect;
use crate::Sizing;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Eq,
PartialEq,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum DefaultLayout {
BSP,
Columns,
@@ -21,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`
}
@@ -35,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;
};
@@ -137,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,
}
}
@@ -150,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

@@ -57,7 +57,9 @@ pub enum SocketMessage {
SendContainerToWorkspaceNumber(usize),
CycleSendContainerToWorkspace(CycleDirection),
SendContainerToMonitorWorkspaceNumber(usize, usize),
MoveContainerToMonitorWorkspaceNumber(usize, usize),
SendContainerToNamedWorkspace(String),
CycleMoveWorkspaceToMonitor(CycleDirection),
MoveWorkspaceToMonitorNumber(usize),
SwapWorkspacesToMonitorNumber(usize),
ForceFocus,
@@ -65,6 +67,7 @@ pub enum SocketMessage {
Minimize,
Promote,
PromoteFocus,
PromoteWindow(OperationDirection),
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
@@ -129,11 +132,22 @@ pub enum SocketMessage {
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
ActiveWindowBorder(bool),
ActiveWindowBorderColour(WindowKind, u32, u32, u32),
ActiveWindowBorderWidth(i32),
ActiveWindowBorderOffset(i32),
#[serde(alias = "ActiveWindowBorder")]
Border(bool),
#[serde(alias = "ActiveWindowBorderColour")]
BorderColour(WindowKind, u32, u32, u32),
#[serde(alias = "ActiveWindowBorderStyle")]
BorderStyle(BorderStyle),
BorderWidth(i32),
BorderOffset(i32),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
StackbarLabel(StackbarLabel),
StackbarFocusedTextColour(u32, u32, u32),
StackbarUnfocusedTextColour(u32, u32, u32),
StackbarBackgroundColour(u32, u32, u32),
StackbarHeight(i32),
StackbarTabWidth(i32),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
ResizeDelta(i32),
@@ -148,6 +162,7 @@ pub enum SocketMessage {
IdentifyLayeredApplication(ApplicationIdentifier, String),
IdentifyBorderOverflowApplication(ApplicationIdentifier, String),
State,
GlobalState,
VisibleWindows,
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
@@ -165,6 +180,7 @@ pub enum SocketMessage {
SocketSchema,
StaticConfigSchema,
GenerateStaticConfig,
DebugWindow(isize),
}
impl SocketMessage {
@@ -181,20 +197,48 @@ impl FromStr for SocketMessage {
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
Never,
OnStack,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum WindowKind {
Single,
Stack,
Monocle,
pub enum StackbarLabel {
#[default]
Process,
Title,
}
#[derive(
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
pub enum BorderStyle {
#[default]
/// Use the system border style
System,
/// Use the Windows 11-style rounded borders
Rounded,
/// Use the Windows 10-style square borders
Square,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum WindowKind {
Single,
Stack,
Monocle,
Unfocused,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum StateQuery {
FocusedMonitorIndex,
FocusedWorkspaceIndex,
@@ -215,7 +259,6 @@ pub enum StateQuery {
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum ApplicationIdentifier {
#[serde(alias = "exe")]
Exe,
@@ -223,12 +266,13 @@ pub enum ApplicationIdentifier {
Class,
#[serde(alias = "title")]
Title,
#[serde(alias = "path")]
Path,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
Komorebi,
@@ -239,7 +283,6 @@ pub enum FocusFollowsMouseImplementation {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum WindowContainerBehaviour {
/// Create a new container for each new window
Create,
@@ -250,18 +293,18 @@ pub enum WindowContainerBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
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(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum HidingBehaviour {
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
Hide,
@@ -274,7 +317,6 @@ pub enum HidingBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows
Op,
@@ -285,7 +327,6 @@ pub enum OperationBehaviour {
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum Sizing {
Increase,
Decrease,

View File

@@ -13,7 +13,6 @@ use crate::Axis;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum OperationDirection {
Left,
Right,

View File

@@ -26,13 +26,29 @@ impl From<RECT> for Rect {
}
}
impl From<Rect> for RECT {
fn from(rect: Rect) -> Self {
Self {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
}
}
}
impl Rect {
/// decrease the size of self by the padding amount.
pub fn add_padding(&mut self, padding: i32) {
self.left += padding;
self.top += padding;
self.right -= padding * 2;
self.bottom -= padding * 2;
pub fn add_padding<T>(&mut self, padding: T)
where
T: Into<Option<i32>>,
{
if let Some(padding) = padding.into() {
self.left += padding;
self.top += padding;
self.right -= padding * 2;
self.bottom -= padding * 2;
}
}
/// increase the size of self by the margin amount.
@@ -43,6 +59,14 @@ impl Rect {
self.bottom += margin * 2;
}
pub fn left_padding(&mut self, padding: i32) {
self.left += padding;
}
pub fn right_padding(&mut self, padding: i32) {
self.right -= padding;
}
#[must_use]
pub const fn contains_point(&self, point: (i32, i32)) -> bool {
point.0 >= self.left

14
komorebi-gui/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "komorebi-gui"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
egui_extras = { version = "0.27" }
eframe = "0.27"
komorebi-client = { path = "../komorebi-client" }
serde_json = "1"
random_word = { version = "0.4.3", features = ["en"] }
windows = { workspace = true }

805
komorebi-gui/src/main.rs Normal file
View File

@@ -0,0 +1,805 @@
use eframe::egui;
use eframe::egui::color_picker::Alpha;
use eframe::egui::Color32;
use eframe::egui::ViewportBuilder;
use komorebi_client::BorderStyle;
use komorebi_client::Colour;
use komorebi_client::DefaultLayout;
use komorebi_client::GlobalState;
use komorebi_client::Layout;
use komorebi_client::Rect;
use komorebi_client::Rgb;
use komorebi_client::RuleDebug;
use komorebi_client::SocketMessage;
use komorebi_client::StackbarLabel;
use komorebi_client::StackbarMode;
use komorebi_client::State;
use komorebi_client::Window;
use komorebi_client::WindowKind;
use std::collections::HashMap;
use std::time::Duration;
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
fn main() {
let native_options = eframe::NativeOptions {
viewport: ViewportBuilder::default()
.with_always_on_top()
.with_inner_size([320.0, 500.0]),
follow_system_theme: true,
..Default::default()
};
let _ = eframe::run_native(
"komorebi-gui",
native_options,
Box::new(|cc| Box::new(KomorebiGui::new(cc))),
);
}
struct BorderColours {
single: Color32,
stack: Color32,
monocle: Color32,
unfocused: Color32,
}
struct BorderConfig {
border_enabled: bool,
border_colours: BorderColours,
border_style: BorderStyle,
border_offset: i32,
border_width: i32,
}
struct StackbarConfig {
mode: StackbarMode,
label: StackbarLabel,
height: i32,
width: i32,
focused_text_colour: Color32,
unfocused_text_colour: Color32,
background_colour: Color32,
}
struct MonitorConfig {
size: Rect,
work_area_offset: Rect,
workspaces: Vec<WorkspaceConfig>,
}
impl From<&komorebi_client::Monitor> for MonitorConfig {
fn from(value: &komorebi_client::Monitor) -> Self {
let mut workspaces = vec![];
for ws in value.workspaces() {
workspaces.push(WorkspaceConfig::from(ws));
}
Self {
size: *value.size(),
work_area_offset: value.work_area_offset().unwrap_or_default(),
workspaces,
}
}
}
struct WorkspaceConfig {
name: String,
tile: bool,
layout: DefaultLayout,
container_padding: i32,
workspace_padding: i32,
}
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
fn from(value: &komorebi_client::Workspace) -> Self {
let layout = match value.layout() {
Layout::Default(layout) => *layout,
Layout::Custom(_) => DefaultLayout::BSP,
};
let name = value
.name()
.to_owned()
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
Self {
layout,
name,
tile: *value.tile(),
workspace_padding: value.workspace_padding().unwrap_or(20),
container_padding: value.container_padding().unwrap_or(20),
}
}
}
struct KomorebiGui {
border_config: BorderConfig,
stackbar_config: StackbarConfig,
mouse_follows_focus: bool,
monitors: Vec<MonitorConfig>,
workspace_names: HashMap<usize, Vec<String>>,
debug_hwnd: isize,
debug_windows: Vec<Window>,
debug_rule: Option<RuleDebug>,
}
fn colour32(colour: Option<Colour>) -> Color32 {
match colour {
Some(Colour::Rgb(rgb)) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
Some(Colour::Hex(hex)) => {
let rgb = Rgb::from(hex);
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
}
None => Color32::from_rgb(0, 0, 0),
}
}
impl KomorebiGui {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let global_state: GlobalState = serde_json::from_str(
&komorebi_client::send_query(&SocketMessage::GlobalState).unwrap(),
)
.unwrap();
let state: State =
serde_json::from_str(&komorebi_client::send_query(&SocketMessage::State).unwrap())
.unwrap();
let border_colours = BorderColours {
single: colour32(global_state.border_colours.single),
stack: colour32(global_state.border_colours.stack),
monocle: colour32(global_state.border_colours.monocle),
unfocused: colour32(global_state.border_colours.unfocused),
};
let border_config = BorderConfig {
border_enabled: global_state.border_enabled,
border_colours,
border_style: global_state.border_style,
border_offset: global_state.border_offset,
border_width: global_state.border_width,
};
let mut monitors = vec![];
for m in state.monitors.elements() {
monitors.push(MonitorConfig::from(m));
}
let mut workspace_names = HashMap::new();
for (monitor_idx, m) in monitors.iter().enumerate() {
for ws in &m.workspaces {
let names = workspace_names.entry(monitor_idx).or_insert_with(Vec::new);
names.push(ws.name.clone());
}
}
let stackbar_config = StackbarConfig {
mode: global_state.stackbar_mode,
height: global_state.stackbar_height,
width: global_state.stackbar_tab_width,
label: global_state.stackbar_label,
focused_text_colour: colour32(Some(global_state.stackbar_focused_text_colour)),
unfocused_text_colour: colour32(Some(global_state.stackbar_unfocused_text_colour)),
background_colour: colour32(Some(global_state.stackbar_tab_background_colour)),
};
let mut debug_windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(&mut debug_windows as *mut Vec<Window> as isize),
)
.unwrap();
};
Self {
border_config,
mouse_follows_focus: state.mouse_follows_focus,
monitors,
workspace_names,
debug_hwnd: 0,
debug_windows,
stackbar_config,
debug_rule: None,
}
}
}
extern "system" fn enum_window(
hwnd: windows::Win32::Foundation::HWND,
lparam: windows::Win32::Foundation::LPARAM,
) -> windows::Win32::Foundation::BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let window = Window { hwnd: hwnd.0 };
if window.is_window()
&& !window.is_miminized()
&& window.is_visible()
&& window.title().is_ok()
&& window.exe().is_ok()
{
windows.push(window);
}
true.into()
}
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
let language = "json";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
}
impl eframe::App for KomorebiGui {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ctx.set_pixels_per_point(2.0);
egui::ScrollArea::vertical().show(ui, |ui| {
ui.set_width(ctx.screen_rect().width());
ui.collapsing("Debugging", |ui| {
ui.collapsing("Window Rules", |ui| {
let window = Window {
hwnd: self.debug_hwnd,
};
let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
format!("{title} ({exe})")
} else {
String::from("Select a Window")
};
if ui.button("Refresh Windows").clicked() {
let mut debug_windows = vec![];
unsafe {
EnumWindows(
Some(enum_window),
windows::Win32::Foundation::LPARAM(
&mut debug_windows as *mut Vec<Window> as isize,
),
)
.unwrap();
};
self.debug_windows = debug_windows;
}
egui::ComboBox::from_label("Select a Window")
.selected_text(label)
.show_ui(ui, |ui| {
for w in &self.debug_windows {
if ui
.selectable_value(
&mut self.debug_hwnd,
w.hwnd,
format!(
"{} ({})",
w.title().unwrap(),
w.exe().unwrap()
),
)
.changed()
{
let debug_rule: RuleDebug = serde_json::from_str(
&komorebi_client::send_query(
&SocketMessage::DebugWindow(self.debug_hwnd),
)
.unwrap(),
)
.unwrap();
self.debug_rule = Some(debug_rule)
}
}
});
if let Some(debug_rule) = &self.debug_rule {
json_view_ui(ui, &serde_json::to_string_pretty(debug_rule).unwrap())
}
});
});
ui.collapsing("Mouse", |ui| {
if ui
.toggle_value(&mut self.mouse_follows_focus, "Mouse Follows Focus")
.changed()
{
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
))
.unwrap();
}
});
ui.collapsing("Border", |ui| {
if ui
.toggle_value(&mut self.border_config.border_enabled, "Border")
.changed()
{
komorebi_client::send_message(&SocketMessage::Border(
self.border_config.border_enabled,
))
.unwrap();
}
ui.collapsing("Colours", |ui| {
ui.collapsing("Single", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.single,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Single,
self.border_config.border_colours.single.r() as u32,
self.border_config.border_colours.single.g() as u32,
self.border_config.border_colours.single.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Stack", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.stack,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Stack,
self.border_config.border_colours.stack.r() as u32,
self.border_config.border_colours.stack.g() as u32,
self.border_config.border_colours.stack.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Monocle", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.monocle,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Monocle,
self.border_config.border_colours.monocle.r() as u32,
self.border_config.border_colours.monocle.g() as u32,
self.border_config.border_colours.monocle.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Unfocused", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.unfocused,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Unfocused,
self.border_config.border_colours.unfocused.r() as u32,
self.border_config.border_colours.unfocused.g() as u32,
self.border_config.border_colours.unfocused.b() as u32,
))
.unwrap();
}
})
});
ui.collapsing("Style", |ui| {
for option in [
BorderStyle::System,
BorderStyle::Rounded,
BorderStyle::Square,
] {
if ui
.add(egui::SelectableLabel::new(
self.border_config.border_style == option,
option.to_string(),
))
.clicked()
{
self.border_config.border_style = option;
komorebi_client::send_message(&SocketMessage::BorderStyle(
self.border_config.border_style,
))
.unwrap();
std::thread::sleep(Duration::from_secs(1));
komorebi_client::send_message(&SocketMessage::Retile).unwrap();
}
}
});
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_width,
-50..=50,
))
.changed()
{
komorebi_client::send_message(&SocketMessage::BorderWidth(
self.border_config.border_width,
))
.unwrap();
};
});
ui.collapsing("Offset", |ui| {
if ui
.add(egui::Slider::new(
&mut self.border_config.border_offset,
-50..=50,
))
.changed()
{
komorebi_client::send_message(&SocketMessage::BorderOffset(
self.border_config.border_offset,
))
.unwrap();
};
});
});
ui.collapsing("Stackbar", |ui| {
for option in [
StackbarMode::Never,
StackbarMode::OnStack,
StackbarMode::Always,
] {
if ui
.add(egui::SelectableLabel::new(
self.stackbar_config.mode == option,
option.to_string(),
))
.clicked()
{
self.stackbar_config.mode = option;
komorebi_client::send_message(&SocketMessage::StackbarMode(
self.stackbar_config.mode,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
}
}
ui.collapsing("Label", |ui| {
for option in [StackbarLabel::Process, StackbarLabel::Title] {
if ui
.add(egui::SelectableLabel::new(
self.stackbar_config.label == option,
option.to_string(),
))
.clicked()
{
self.stackbar_config.label = option;
komorebi_client::send_message(&SocketMessage::StackbarLabel(
self.stackbar_config.label,
))
.unwrap();
}
}
});
ui.collapsing("Colours", |ui| {
ui.collapsing("Focused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.focused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarFocusedTextColour(
self.stackbar_config.focused_text_colour.r() as u32,
self.stackbar_config.focused_text_colour.g() as u32,
self.stackbar_config.focused_text_colour.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Unfocused Text", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.unfocused_text_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarUnfocusedTextColour(
self.stackbar_config.unfocused_text_colour.r() as u32,
self.stackbar_config.unfocused_text_colour.g() as u32,
self.stackbar_config.unfocused_text_colour.b() as u32,
),
)
.unwrap();
}
});
ui.collapsing("Background", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.stackbar_config.background_colour,
Alpha::Opaque,
) {
komorebi_client::send_message(
&SocketMessage::StackbarBackgroundColour(
self.stackbar_config.background_colour.r() as u32,
self.stackbar_config.background_colour.g() as u32,
self.stackbar_config.background_colour.b() as u32,
),
)
.unwrap();
}
})
});
ui.collapsing("Width", |ui| {
if ui
.add(egui::Slider::new(&mut self.stackbar_config.width, 0..=500))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarTabWidth(
self.stackbar_config.width,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
};
});
ui.collapsing("Height", |ui| {
if ui
.add(egui::Slider::new(&mut self.stackbar_config.height, 0..=100))
.drag_stopped()
{
komorebi_client::send_message(&SocketMessage::StackbarHeight(
self.stackbar_config.height,
))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
};
});
});
for (monitor_idx, monitor) in self.monitors.iter_mut().enumerate() {
ui.collapsing(
format!(
"Monitor {monitor_idx} ({}x{})",
monitor.size.right, monitor.size.bottom
),
|ui| {
ui.collapsing("Work Area Offset", |ui| {
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.left,
0..=500,
)
.text("Left"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.top,
0..=500,
)
.text("Top"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.right,
0..=500,
)
.text("Right"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
if ui
.add(
egui::Slider::new(
&mut monitor.work_area_offset.bottom,
0..=500,
)
.text("Bottom"),
)
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
monitor.work_area_offset,
),
)
.unwrap();
};
});
ui.collapsing("Workspaces", |ui| {
for (workspace_idx, workspace) in
monitor.workspaces.iter_mut().enumerate()
{
ui.collapsing(
format!("Workspace {workspace_idx} ({})", workspace.name),
|ui| {
if ui.button("Focus").clicked() {
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(false),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
monitor_idx,
workspace_idx,
),
)
.unwrap();
komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(
self.mouse_follows_focus,
),
)
.unwrap();
}
if ui
.toggle_value(&mut workspace.tile, "Tiling")
.changed()
{
komorebi_client::send_message(
&SocketMessage::WorkspaceTiling(
monitor_idx,
workspace_idx,
workspace.tile,
),
)
.unwrap();
}
ui.collapsing("Name", |ui| {
let monitor_workspaces = self
.workspace_names
.get_mut(&monitor_idx)
.unwrap();
let workspace_name =
&mut monitor_workspaces[workspace_idx];
if ui
.text_edit_singleline(workspace_name)
.lost_focus()
{
workspace.name = workspace_name.clone();
komorebi_client::send_message(
&SocketMessage::WorkspaceName(
monitor_idx,
workspace_idx,
workspace.name.clone(),
),
)
.unwrap();
}
});
ui.collapsing("Layout", |ui| {
for option in [
DefaultLayout::BSP,
DefaultLayout::Columns,
DefaultLayout::Rows,
DefaultLayout::VerticalStack,
DefaultLayout::HorizontalStack,
DefaultLayout::UltrawideVerticalStack,
DefaultLayout::Grid,
] {
if ui
.add(egui::SelectableLabel::new(
workspace.layout == option,
option.to_string(),
))
.clicked()
{
workspace.layout = option;
komorebi_client::send_message(
&SocketMessage::WorkspaceLayout(
monitor_idx,
workspace_idx,
workspace.layout,
),
)
.unwrap();
}
}
});
ui.collapsing("Container Padding", |ui| {
if ui
.add(egui::Slider::new(
&mut workspace.container_padding,
0..=100,
))
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::ContainerPadding(
monitor_idx,
workspace_idx,
workspace.container_padding,
),
)
.unwrap();
};
});
ui.collapsing("Workspace Padding", |ui| {
if ui
.add(egui::Slider::new(
&mut workspace.workspace_padding,
0..=100,
))
.drag_stopped()
{
komorebi_client::send_message(
&SocketMessage::WorkspacePadding(
monitor_idx,
workspace_idx,
workspace.workspace_padding,
),
)
.unwrap();
};
});
},
);
}
});
},
);
}
});
});
}
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/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,
"active_window_border": false,
"active_window_border_colours": {
"single": { "r": 66, "g": 165, "b": 245 },
"stack": { "r": 256, "g": 165, "b": 66 },
"monocle": { "r": 255, "g": 51, "b": 153 }
},
"monitors": [
{
"workspaces": [
{ "name": "I", "layout": "BSP" },
{ "name": "II", "layout": "VerticalStack" },
{ "name": "III", "layout": "HorizontalStack" },
{ "name": "IV", "layout": "UltrawideVerticalStack" },
{ "name": "V", "layout": "Rows" }
]
}
]
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.22-dev.0"
version = "0.1.27-dev.0"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
@@ -13,7 +13,7 @@ edition = "2021"
[dependencies]
komorebi-core = { path = "../komorebi-core" }
bitflags = "2"
bitflags = { version = "2", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
color-eyre = { workspace = true }
crossbeam-channel = "0.5"
@@ -22,20 +22,20 @@ ctrlc = "3"
dirs = { workspace = true }
getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
hotwatch = "0.4"
hotwatch = "0.5"
lazy_static = "1"
miow = "0.5"
miow = "0.6"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.7"
os_info = "3.8"
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
paste = "1"
regex = "1"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_json = { workspace = true }
strum = { version = "0.26", features = ["derive"] }
sysinfo = "0.30"
sysinfo = { workspace = true }
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ -48,5 +48,7 @@ windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.52"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data" }
[features]
deadlock_detection = []

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,226 @@
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::BorderStyle;
use komorebi_core::Rect;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::time::Duration;
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);
std::thread::sleep(Duration::from_millis(10));
}
}
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() {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
}
BorderStyle::Rounded => {
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
}
BorderStyle::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,371 @@
#![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::BorderStyle;
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<BorderStyle>> = Arc::new(Mutex::new(BorderStyle::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();
'monitors: 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 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,
if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
},
);
}
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd(),
)?;
border.update(&rect)?;
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
for (id, b) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& border_hwnd != b.hwnd
{
b.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
borders.remove(id);
}
continue 'monitors;
}
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 'monitors;
}
// 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

@@ -10,7 +10,6 @@ use interfaces::IServiceProvider;
use std::ffi::c_void;
use windows::core::ComInterface;
use windows::core::Interface;
use windows::Win32::Foundation::HWND;
use windows::Win32::System::Com::CoCreateInstance;

View File

@@ -34,6 +34,30 @@ impl PartialEq for Container {
}
impl Container {
pub fn hide(&self, omit: Option<isize>) {
for window in self.windows().iter().rev() {
let mut should_hide = omit.is_none();
if !should_hide {
if let Some(omit) = omit {
if omit != window.hwnd {
should_hide = true
}
}
}
if should_hide {
window.hide();
}
}
}
pub fn restore(&self) {
if let Some(window) = self.focused_window() {
window.restore();
}
}
pub fn load_focused_window(&mut self) {
let focused_idx = self.focused_window_idx();
for (i, window) in self.windows_mut().iter_mut().enumerate() {

View File

@@ -1,76 +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::windows_callbacks;
use crate::WindowsApi;
use crate::HIDDEN_HWND;
use crate::TRANSPARENCY_COLOUR;
#[derive(Debug, Clone, Copy)]
pub struct Hidden {
pub(crate) hwnd: isize,
}
impl From<isize> for Hidden {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Hidden {
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::hidden_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_hidden_window(PCWSTR(name_cl.as_ptr()), instance)?;
let hidden = Self::from(hwnd);
let mut message = MSG::default();
unsafe {
while GetMessageW(&mut message, hidden.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()) };
}
HIDDEN_HWND.store(hwnd.0, Ordering::SeqCst);
Ok(())
}
}

View File

@@ -1,15 +1,17 @@
pub mod border;
pub mod border_manager;
pub mod com;
#[macro_use]
pub mod ring;
pub mod colour;
pub mod container;
pub mod hidden;
pub mod monitor;
pub mod monitor_reconciliator;
pub mod process_command;
pub mod process_event;
pub mod process_movement;
pub mod reaper;
pub mod set_window_position;
pub mod stackbar_manager;
pub mod static_config;
pub mod styles;
pub mod window;
@@ -20,9 +22,11 @@ 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;
use std::collections::VecDeque;
use std::fs::File;
use std::io::Write;
use std::net::TcpStream;
@@ -30,15 +34,15 @@ use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicIsize;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use hidden::*;
pub use colour::*;
pub use process_command::*;
pub use process_event::*;
pub use static_config::*;
pub use window::*;
pub use window_manager::*;
pub use window_manager_event::*;
pub use windows_api::WindowsApi;
@@ -46,6 +50,7 @@ pub use windows_api::*;
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::ApplicationIdentifier;
use komorebi_core::HidingBehaviour;
@@ -66,57 +71,57 @@ type WorkspaceRule = (usize, usize, bool);
lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
IdWithIdentifier {
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("steam.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
}),
]));
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> =
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> =
Arc::new(Mutex::new(vec![
IdWithIdentifier {
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("explorer.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("chrome.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("idea64.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("ApplicationFrameHost.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("steam.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}
})
]));
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
IdWithIdentifier {
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("firefox.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: String::from("idea64.exe"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
}),
]));
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -126,25 +131,25 @@ lazy_static! {
Arc::new(Mutex::new(HashMap::new()));
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
// mstsc.exe creates these on Windows 11 when a WSL process is launched
// https://github.com/LGUG2Z/komorebi/issues/74
IdWithIdentifier {
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: String::from("OPContainerClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
}),
MatchingRule::Simple(IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: String::from("IHWindowClass"),
matching_strategy: Option::from(MatchingStrategy::Equals),
}
})
]));
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"Chrome_RenderWidgetHostHWND".to_string(),
]));
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"X410.exe".to_string(),
"vcxsrv.exe".to_string(),
@@ -170,7 +175,7 @@ lazy_static! {
}
})
};
static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
pub static ref AHK_EXE: String = {
let mut ahk: String = String::from("autohotkey.exe");
@@ -189,14 +194,13 @@ lazy_static! {
)
};
static ref BORDER_RECT: Arc<Mutex<Rect>> =
Arc::new(Mutex::new(Rect::default()));
static ref BORDER_OFFSET: AtomicI32 = Default::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![]));
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
@@ -205,19 +209,8 @@ 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(20);
// 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);
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {

View File

@@ -23,17 +23,22 @@ use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use komorebi::hidden::Hidden;
use komorebi::border_manager;
use komorebi::load_configuration;
use komorebi::monitor_reconciliator;
use komorebi::process_command::listen_for_commands;
use komorebi::process_command::listen_for_commands_tcp;
use komorebi::process_event::listen_for_events;
use komorebi::process_movement::listen_for_movements;
use komorebi::reaper;
use komorebi::stackbar_manager;
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;
use komorebi::INITIAL_CONFIGURATION_LOADED;
use komorebi::SESSION_ID;
@@ -49,8 +54,8 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
std::env::set_var("RUST_LOG", "info");
}
let appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi_plaintext.log");
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
let color_appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi.log");
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
@@ -185,8 +190,6 @@ fn main() -> Result<()> {
#[cfg(feature = "deadlock_detection")]
detect_deadlocks();
Hidden::create("komorebi-hidden")?;
let static_config = opts.config.map_or_else(
|| {
let komorebi_json = HOME_DIR.join("komorebi.json");
@@ -199,6 +202,8 @@ fn main() -> Result<()> {
Option::from,
);
std::fs::create_dir_all(&*DATA_DIR)?;
let wm = if let Some(config) = &static_config {
tracing::info!(
"creating window manager from static configuration file: {}",
@@ -250,6 +255,12 @@ fn main() -> Result<()> {
listen_for_movements(wm.clone());
}
border_manager::listen_for_notifications(wm.clone());
stackbar_manager::listen_for_notifications(wm.clone());
workspace_reconciliator::listen_for_notifications(wm.clone());
monitor_reconciliator::listen_for_notifications(wm.clone())?;
reaper::watch_for_orphans(wm.clone());
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
ctrlc_sender

View File

@@ -27,15 +27,19 @@ pub struct Monitor {
#[getset(get = "pub", set = "pub")]
name: String,
#[getset(get = "pub", set = "pub")]
device: Option<String>,
device: String,
#[getset(get = "pub", set = "pub")]
device_id: Option<String>,
device_id: String,
#[getset(get = "pub", set = "pub")]
size: Rect,
#[getset(get = "pub", set = "pub")]
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")]
@@ -46,18 +50,27 @@ pub struct Monitor {
impl_ring_elements!(Monitor, Workspace);
pub fn new(id: isize, size: Rect, work_area_size: Rect, name: String) -> Monitor {
pub fn new(
id: isize,
size: Rect,
work_area_size: Rect,
name: String,
device: String,
device_id: String,
) -> Monitor {
let mut workspaces = Ring::default();
workspaces.elements_mut().push_back(Workspace::default());
Monitor {
id,
name,
device: None,
device_id: None,
device,
device_id,
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(),
@@ -71,7 +84,7 @@ impl Monitor {
if i == focused_idx {
workspace.restore(mouse_follows_focus)?;
} else {
workspace.hide();
workspace.hide(None);
}
}
@@ -194,6 +207,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 +220,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

@@ -0,0 +1,190 @@
use std::sync::mpsc;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
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::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
use crate::monitor_reconciliator;
use crate::WindowsApi;
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
#[derive(Debug, Clone, Copy)]
pub struct Hidden {
pub hwnd: isize,
}
impl From<isize> for Hidden {
fn from(hwnd: isize) -> Self {
Self { hwnd }
}
}
impl Hidden {
pub const fn hwnd(self) -> HWND {
HWND(self.hwnd)
}
pub fn create(name: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("{name}\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_hidden_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(())
});
let hwnd = hwnd_receiver.recv()?;
WindowsApi::wts_register_session_notification(hwnd)?;
Ok(Self { hwnd })
}
pub extern "system" fn callback(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_POWERBROADCAST => {
match wparam.0 as u32 {
// Automatic: System resumed itself from sleep or hibernation
// Suspend: User resumed system from sleep or hibernation
PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => {
tracing::debug!(
"WM_POWERBROADCAST event received - resume from suspend"
);
let _ = monitor_reconciliator::event_tx().send(
monitor_reconciliator::Notification::ResumingFromSuspendedState,
);
LRESULT(0)
}
// Computer is entering a suspended state
PBT_APMSUSPEND => {
tracing::debug!(
"WM_POWERBROADCAST event received - entering suspended state"
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::EnteringSuspendedState);
LRESULT(0)
}
_ => LRESULT(0),
}
}
WM_WTSSESSION_CHANGE => {
match wparam.0 as u32 {
WTS_SESSION_LOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::SessionLocked);
}
WTS_SESSION_UNLOCK => {
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::SessionUnlocked);
}
_ => {}
}
LRESULT(0)
}
// This event gets sent when:
// - The scaling factor on a display changes
// - The resolution on a display changes
// - A monitor is added
// - A monitor is removed
// Since WM_DEVICECHANGE also notifies on monitor changes, we only handle scaling
// and resolution changes here
WM_DISPLAYCHANGE => {
tracing::debug!(
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::ResolutionScalingChanged);
LRESULT(0)
}
// Unfortunately this is the event sent with ButteryTaskbar which I use a lot
// Original idea from https://stackoverflow.com/a/33762334
WM_SETTINGCHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
tracing::debug!(
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::WorkAreaChanged);
}
LRESULT(0)
}
// This event + wparam combo is sent 4 times when a monitor is added based on my testing on win11
// Original idea from https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
tracing::debug!(
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
);
let _ = monitor_reconciliator::event_tx()
.send(monitor_reconciliator::Notification::DisplayConnectionChange);
}
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
}

View File

@@ -0,0 +1,404 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
use crate::MonitorConfig;
use crate::WindowManager;
use crate::WindowsApi;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::Rect;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
pub mod hidden;
pub enum Notification {
ResolutionScalingChanged,
WorkAreaChanged,
DisplayConnectionChange,
EnteringSuspendedState,
ResumingFromSuspendedState,
SessionLocked,
SessionUnlocked,
}
static ACTIVE: AtomicBool = AtomicBool::new(true);
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = 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 insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
monitor_cache.insert(device_id.to_string(), config);
}
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
Ok(win32_display_data::connected_displays()
.flatten()
.map(|display| {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
monitor::new(
display.hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
)
})
.collect::<Vec<_>>())
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
#[allow(clippy::expect_used)]
Hidden::create("komorebi-hidden")?;
tracing::info!("created hidden window to listen for monitor-related events");
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)
}
}
}
});
Ok(())
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
'receiver: for notification in receiver {
if !ACTIVE.load_consume() {
if matches!(
notification,
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked
) {
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
);
ACTIVE.store(true, Ordering::SeqCst);
}
continue 'receiver;
}
let mut wm = wm.lock();
match notification {
Notification::EnteringSuspendedState | Notification::SessionLocked => {
tracing::debug!(
"deactivating reconciliator until system resumes from suspended state or session is unlocked"
);
ACTIVE.store(false, Ordering::SeqCst);
}
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => {
// this is only handled above if the reconciliator is paused
}
Notification::WorkAreaChanged => {
tracing::debug!("handling work area changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
let mut should_update = false;
// Update work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
}
if should_update {
tracing::info!("updated work area for {}", monitor.device_id());
monitor.update_focused_workspace(offset)?;
border_manager::event_tx().send(border_manager::Notification)?;
} else {
tracing::debug!(
"work areas match, reconciliation not required for {}",
monitor.device_id()
);
}
}
}
Notification::ResolutionScalingChanged => {
tracing::debug!("handling resolution/scaling changed notification");
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
let mut should_update = false;
// Update sizes and work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
if reference.size() != monitor.size() {
monitor.set_size(Rect {
left: reference.size().left,
top: reference.size().top,
right: reference.size().right,
bottom: reference.size().bottom,
});
should_update = true;
}
}
if should_update {
tracing::info!(
"updated monitor resolution/scaling for {}",
monitor.device_id()
);
monitor.update_focused_workspace(offset)?;
border_manager::event_tx().send(border_manager::Notification)?;
} else {
tracing::debug!(
"resolutions match, reconciliation not required for {}",
monitor.device_id()
);
}
}
}
Notification::DisplayConnectionChange => {
tracing::debug!("handling display connection change notification");
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
let initial_monitor_count = wm.monitors().len();
// Get the currently attached display devices
let attached_devices = attached_display_devices()?;
// Make sure that in our state any attached displays have the latest Win32 data
for monitor in wm.monitors_mut() {
for attached in &attached_devices {
if attached.device_id().eq(monitor.device_id()) {
monitor.set_id(attached.id());
monitor.set_name(attached.name().clone());
monitor.set_size(*attached.size());
monitor.set_work_area_size(*attached.work_area_size());
}
}
}
if initial_monitor_count == attached_devices.len() {
tracing::debug!("monitor counts match, reconciliation not required");
continue 'receiver;
}
if attached_devices.is_empty() {
tracing::debug!(
"no devices found, skipping reconciliation to avoid breaking state"
);
continue 'receiver;
}
if initial_monitor_count > attached_devices.len() {
tracing::info!(
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
attached_devices.len()
);
// Gather all the containers that will be orphaned from disconnected and invalid displays
let mut orphaned_containers = vec![];
// Collect the ids in our state which aren't in the current attached display ids
// These are monitors that have been removed
let mut newly_removed_displays = vec![];
for m in wm.monitors().iter() {
if !attached_devices
.iter()
.any(|attached| attached.device_id().eq(m.device_id()))
{
newly_removed_displays.push(m.device_id().clone());
for workspace in m.workspaces() {
for container in workspace.containers() {
// Save the orphaned containers from the removed monitor
orphaned_containers.push(container.clone());
}
}
// Let's add their state to the cache for later
monitor_cache.insert(m.device_id().clone(), m.into());
}
}
if !orphaned_containers.is_empty() {
tracing::info!(
"removed orphaned containers from: {newly_removed_displays:?}"
);
}
if !newly_removed_displays.is_empty() {
// After we have cached them, remove them from our state
wm.monitors_mut()
.retain(|m| !newly_removed_displays.contains(m.device_id()));
}
let post_removal_monitor_count = wm.monitors().len();
let focused_monitor_idx = wm.focused_monitor_idx();
if focused_monitor_idx >= post_removal_monitor_count {
wm.focus_monitor(0)?;
}
if !orphaned_containers.is_empty() {
if let Some(primary) = wm.monitors_mut().front_mut() {
if let Some(focused_ws) = primary.focused_workspace_mut() {
let focused_container_idx = focused_ws.focused_container_idx();
// Put the orphaned containers somewhere visible
for container in orphaned_containers {
focused_ws.add_container(container);
}
// Gotta reset the focus or the movement will feel "off"
if initial_monitor_count != post_removal_monitor_count {
focused_ws.focus_container(focused_container_idx);
}
}
}
}
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
// If we have lost a monitor, update everything to filter out any jank
if initial_monitor_count != post_removal_monitor_count {
monitor.update_focused_workspace(offset)?;
}
}
}
let post_removal_monitor_count = wm.monitors().len();
// This is the list of device ids after we have removed detached displays
let post_removal_device_ids = wm
.monitors()
.iter()
.map(Monitor::device_id)
.cloned()
.collect::<Vec<_>>();
// Check for and add any new monitors that may have been plugged in
// Monitor and display index preferences get applied in this function
WindowsApi::load_monitor_information(&mut wm.monitors)?;
let post_addition_monitor_count = wm.monitors().len();
if post_addition_monitor_count > post_removal_monitor_count {
tracing::info!(
"monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors",
);
// Look in the updated state for new monitors
for m in wm.monitors_mut() {
let device_id = m.device_id().clone();
// We identify a new monitor when we encounter a new device id
if !post_removal_device_ids.contains(&device_id) {
let mut cache_hit = false;
// Check if that device id exists in the cache for this session
if let Some(cached) = monitor_cache.get(&device_id) {
cache_hit = true;
tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying");
// If it does, load all the monitor settings from the cache entry
m.ensure_workspace_count(cached.workspaces.len());
m.set_work_area_offset(cached.work_area_offset);
m.set_window_based_work_area_offset(
cached.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
cached.window_based_work_area_offset_limit.unwrap_or(1),
);
for (w_idx, workspace) in m.workspaces_mut().iter_mut().enumerate()
{
if let Some(cached_workspace) = cached.workspaces.get(w_idx) {
workspace.load_static_config(cached_workspace)?;
}
}
}
// Entries in the cache should only be used once; remove the entry there was a cache hit
if cache_hit {
monitor_cache.remove(&device_id);
}
}
}
}
let final_count = wm.monitors().len();
if post_removal_monitor_count != final_count {
wm.retile_all(true)?;
// Second retile to fix DPI/resolution related jank
wm.retile_all(true)?;
// Border updates to fix DPI/resolution related jank
border_manager::event_tx().send(border_manager::Notification)?;
}
}
}
}
Ok(())
}

View File

@@ -23,6 +23,7 @@ use uds_windows::UnixStream;
use komorebi_core::config_generation::ApplicationConfiguration;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::Axis;
@@ -37,27 +38,21 @@ 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;
use crate::stackbar_manager;
use crate::static_config::StaticConfig;
use crate::window::RuleDebug;
use crate::window::Window;
use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::GlobalState;
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_OVERFLOW_IDENTIFIERS;
use crate::BORDER_WIDTH;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
@@ -75,6 +70,13 @@ use crate::SUBSCRIPTION_SOCKETS;
use crate::TCP_CONNECTIONS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use stackbar_manager::STACKBAR_LABEL;
use stackbar_manager::STACKBAR_MODE;
use stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use stackbar_manager::STACKBAR_TAB_HEIGHT;
use stackbar_manager::STACKBAR_TAB_WIDTH;
use stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
#[tracing::instrument]
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
@@ -165,21 +167,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() {
@@ -200,6 +187,10 @@ impl WindowManager {
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
SocketMessage::PromoteWindow(direction) => {
self.focus_container_in_direction(direction)?;
self.promote_container_to_front()?
}
SocketMessage::FocusWindow(direction) => {
self.focus_container_in_direction(direction)?;
}
@@ -216,6 +207,7 @@ impl WindowManager {
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
SocketMessage::CycleStack(direction) => {
self.cycle_container_window_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::ForceFocus => {
let focused_window = self.focused_window()?;
@@ -223,8 +215,18 @@ impl WindowManager {
WindowsApi::center_cursor_in_rect(&focused_window_rect)?;
WindowsApi::left_click();
}
SocketMessage::Close => self.focused_window()?.close()?,
SocketMessage::Minimize => self.focused_window()?.minimize(),
SocketMessage::Close => {
Window {
hwnd: WindowsApi::foreground_window()?,
}
.close()?;
}
SocketMessage::Minimize => {
Window {
hwnd: WindowsApi::foreground_window()?,
}
.minimize();
}
SocketMessage::ToggleFloat => self.toggle_float()?,
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
@@ -273,17 +275,19 @@ impl WindowManager {
let mut should_push = true;
for m in &*manage_identifiers {
if m.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(m) = m {
if m.id.eq(id) {
should_push = false;
}
}
}
if should_push {
manage_identifiers.push(IdWithIdentifier {
manage_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::FloatRule(identifier, ref id) => {
@@ -291,17 +295,19 @@ impl WindowManager {
let mut should_push = true;
for f in &*float_identifiers {
if f.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(f) = f {
if f.id.eq(id) {
should_push = false;
}
}
}
if should_push {
float_identifiers.push(IdWithIdentifier {
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
let offset = self.work_area_offset;
@@ -315,6 +321,11 @@ impl WindowManager {
{
for window in container.windows() {
match identifier {
ApplicationIdentifier::Path => {
if window.path()? == *id {
hwnds_to_purge.push((i, window.hwnd));
}
}
ApplicationIdentifier::Exe => {
if window.exe()? == *id {
hwnds_to_purge.push((i, window.hwnd));
@@ -445,6 +456,9 @@ impl WindowManager {
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
}
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
}
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
@@ -467,6 +481,15 @@ impl WindowManager {
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
self.move_workspace_to_monitor(monitor_idx)?;
}
SocketMessage::CycleMoveWorkspaceToMonitor(direction) => {
let monitor_idx = direction.next_idx(
self.focused_monitor_idx(),
NonZeroUsize::new(self.monitors().len())
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.move_workspace_to_monitor(monitor_idx)?;
}
SocketMessage::TogglePause => {
if self.is_paused {
tracing::info!("resuming");
@@ -488,13 +511,16 @@ impl WindowManager {
);
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::FocusMonitorNumber(monitor_idx) => {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::Retile => {
border_manager::destroy_all_borders()?;
self.retile_all(false)?
}
SocketMessage::Retile => 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)?,
@@ -617,10 +643,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
@@ -644,10 +666,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
@@ -657,11 +675,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
@@ -681,14 +697,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)) =
@@ -697,10 +716,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!(
@@ -755,16 +770,25 @@ impl WindowManager {
tracing::info!("replying to state done");
}
SocketMessage::GlobalState => {
let state = match serde_json::to_string_pretty(&GlobalState::default()) {
Ok(state) => state,
Err(error) => error.to_string(),
};
tracing::info!("replying to global state");
reply.write_all(state.as_bytes())?;
tracing::info!("replying to global state done");
}
SocketMessage::VisibleWindows => {
let mut monitor_visible_windows = HashMap::new();
for (index, monitor) in self.monitors().iter().enumerate() {
for monitor in self.monitors() {
if let Some(ws) = monitor.focused_workspace() {
monitor_visible_windows.insert(
monitor
.device_id()
.clone()
.unwrap_or_else(|| format!("{index}")),
monitor.device_id().clone(),
ws.visible_window_details().clone(),
);
}
@@ -903,7 +927,7 @@ impl WindowManager {
}
}
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
if !CUSTOM_FFM.load(Ordering::SeqCst) {
@@ -1009,63 +1033,49 @@ impl WindowManager {
SocketMessage::CompleteConfiguration => {
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
}
SocketMessage::WatchConfiguration(enable) => {
self.watch_configuration(enable)?;
}
SocketMessage::IdentifyBorderOverflowApplication(identifier, ref id) => {
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
}
}
if should_push {
identifiers.push(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}
}
SocketMessage::IdentifyObjectNameChangeApplication(identifier, ref id) => {
let mut identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(IdWithIdentifier {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::IdentifyTrayApplication(identifier, ref id) => {
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(IdWithIdentifier {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::IdentifyLayeredApplication(identifier, ref id) => {
@@ -1073,17 +1083,19 @@ impl WindowManager {
let mut should_push = true;
for i in &*identifiers {
if i.id.eq(id) {
should_push = false;
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(IdWithIdentifier {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
});
}));
}
}
SocketMessage::ManageFocusedWindow => {
@@ -1128,7 +1140,7 @@ impl WindowManager {
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::Save(ref path) => {
let workspace = self.focused_workspace_mut()?;
@@ -1151,7 +1163,7 @@ impl WindowManager {
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
workspace.set_resize_dimensions(resize);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::AddSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
@@ -1206,6 +1218,7 @@ impl WindowManager {
MoveBehaviour::Insert => {
self.cross_monitor_move_behaviour = MoveBehaviour::Swap;
}
_ => {}
}
}
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {
@@ -1214,45 +1227,56 @@ impl WindowManager {
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
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()?;
SocketMessage::Border(enable) => {
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::BorderColour(kind, r, g, b) => match kind {
WindowKind::Single => {
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), 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);
}
WindowKind::Stack => {
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowsApi::invalidate_border_rect()?;
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::BorderStyle(style) => {
let mut border_style = STYLE.lock();
*border_style = style;
}
SocketMessage::ActiveWindowBorderWidth(width) => {
BORDER_WIDTH.store(width, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
SocketMessage::BorderWidth(width) => {
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
}
SocketMessage::ActiveWindowBorderOffset(offset) => {
BORDER_OFFSET.store(offset, Ordering::SeqCst);
WindowsApi::invalidate_border_rect()?;
SocketMessage::BorderOffset(offset) => {
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
}
SocketMessage::AltFocusHack(_) => {
tracing::info!("this action is deprecated");
SocketMessage::StackbarMode(mode) => {
STACKBAR_MODE.store(mode);
}
SocketMessage::StackbarLabel(label) => {
STACKBAR_LABEL.store(label);
}
SocketMessage::StackbarFocusedTextColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_FOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarUnfocusedTextColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarBackgroundColour(r, g, b) => {
let rgb = Rgb::new(r, g, b);
STACKBAR_TAB_BACKGROUND_COLOUR.store(rgb.into(), Ordering::SeqCst);
}
SocketMessage::StackbarHeight(height) => {
STACKBAR_TAB_HEIGHT.store(height, Ordering::SeqCst);
}
SocketMessage::StackbarTabWidth(width) => {
STACKBAR_TAB_WIDTH.store(width, Ordering::SeqCst);
}
SocketMessage::ApplicationSpecificConfigurationSchema => {
let asc = schema_for!(Vec<ApplicationConfiguration>);
@@ -1299,132 +1323,35 @@ impl WindowManager {
SocketMessage::ToggleTitleBars => {
let current = REMOVE_TITLEBARS.load(Ordering::SeqCst);
REMOVE_TITLEBARS.store(!current, Ordering::SeqCst);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
SocketMessage::DebugWindow(hwnd) => {
let window = Window { hwnd };
let mut rule_debug = RuleDebug::default();
let _ = window.should_manage(None, &mut rule_debug);
let schema = serde_json::to_string_pretty(&rule_debug)?;
reply.write_all(schema.as_bytes())?;
}
// Deprecated commands
SocketMessage::AltFocusHack(_)
| 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::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::ActiveWindowBorderOffset(_)
// 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(_) => {
let foreground = WindowsApi::foreground_window()?;
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,
);
}
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)?;
stackbar_manager::event_tx().send(stackbar_manager::Notification)?;
tracing::info!("processed");
Ok(())
}
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
fn handle_initial_workspace_rules(
&mut self,
id: &String,
@@ -1436,7 +1363,7 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
fn handle_definitive_workspace_rules(
&mut self,
id: &String,
@@ -1448,7 +1375,7 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(self), level = "debug")]
pub fn handle_workspace_rules(
&mut self,
id: &String,
@@ -1483,9 +1410,10 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
if wm.is_paused {
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message, &mut stream)?)
}
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())
@@ -1494,10 +1422,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(())
@@ -1532,9 +1456,10 @@ pub fn read_commands_tcp(
if wm.is_paused {
return match message {
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
Ok(wm.process_command(message, stream)?)
}
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())
@@ -1543,10 +1468,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,10 +1,11 @@
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;
use crossbeam_channel::select;
use parking_lot::Mutex;
use komorebi_core::OperationDirection;
@@ -12,22 +13,23 @@ 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::stackbar_manager;
use crate::window::should_act;
use crate::window::RuleDebug;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
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::DATA_DIR;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
@@ -40,12 +42,14 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
tracing::info!("listening");
loop {
select! {
recv(receiver) -> mut maybe_event => {
if let Ok(event) = maybe_event.as_mut() {
match wm.lock().process_event(event) {
Ok(()) => {},
Err(error) => tracing::error!("{}", error)
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)
}
}
}
@@ -57,12 +61,22 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
impl WindowManager {
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[tracing::instrument(skip(self))]
pub fn process_event(&mut self, event: &mut WindowManagerEvent) -> Result<()> {
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
if self.is_paused {
tracing::trace!("ignoring while paused");
return Ok(());
}
let mut rule_debug = RuleDebug::default();
let should_manage = event.window().should_manage(Some(event), &mut rule_debug)?;
// 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 {
return Ok(());
}
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
if let Some(id) = current_virtual_desktop() {
if id != *virtual_desktop_id {
@@ -79,57 +93,38 @@ impl WindowManager {
match event {
WindowManagerEvent::FocusChange(_, window)
| WindowManagerEvent::Show(_, window)
| WindowManagerEvent::DisplayChange(window)
| WindowManagerEvent::MoveResizeEnd(_, window) => {
self.reconcile_monitors()?;
let monitor_idx = self.monitor_idx_from_window(*window)
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
// This is a hidden window apparently associated with COM support mechanisms (based
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
//
// The hidden window, OLEChannelWnd, associated with this class (spawned by
// explorer.exe), after some debugging, is observed to always be tied to the primary
// display monitor, or (usually) monitor 0 in the WindowManager state.
//
// Due to this, at least one user in the Discord has witnessed behaviour where, when
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
// set repeatedly to 0, regardless of where the current foreground window is actually
// located.
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if window.class()? != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
if let Some(monitor_idx) = self.monitor_idx_from_window(window) {
// This is a hidden window apparently associated with COM support mechanisms (based
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
//
// The hidden window, OLEChannelWnd, associated with this class (spawned by
// explorer.exe), after some debugging, is observed to always be tied to the primary
// display monitor, or (usually) monitor 0 in the WindowManager state.
//
// Due to this, at least one user in the Discord has witnessed behaviour where, when
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
// set repeatedly to 0, regardless of where the current foreground window is actually
// located.
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if let Ok(class) = window.class() {
if class != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
}
}
}
}
_ => {}
}
let offset = self.work_area_offset;
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let offset = if monitor.work_area_offset().is_some() {
monitor.work_area_offset()
} else {
offset
};
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
if let WindowManagerEvent::FocusChange(_, window) = event {
let _ = workspace.focus_changed(window.hwnd);
}
}
}
@@ -145,16 +140,18 @@ impl WindowManager {
match event {
WindowManagerEvent::Raise(window) => {
window.raise();
window.focus(false)?;
self.has_pending_raise_op = false;
}
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
if self.focused_workspace()?.contains_window(window.hwnd) {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
already_moved_window_handles.remove(&window.hwnd);
already_moved_window_handles.remove(&window.hwnd);
}
}
WindowManagerEvent::Minimize(_, window) => {
let mut hide = false;
@@ -168,7 +165,7 @@ impl WindowManager {
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
}
WindowManagerEvent::Hide(_, window) => {
@@ -185,6 +182,7 @@ impl WindowManager {
let title = &window.title()?;
let exe_name = &window.exe()?;
let class = &window.class()?;
let path = &window.path()?;
// We don't want to purge windows that have been deliberately hidden by us, eg. when
// they are not on the top of a container stack.
@@ -193,9 +191,11 @@ impl WindowManager {
title,
exe_name,
class,
path,
&tray_and_multi_window_identifiers,
&regex_identifiers,
);
)
.is_some();
if !window.is_window()
|| should_act
@@ -207,7 +207,7 @@ impl WindowManager {
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
@@ -215,6 +215,8 @@ impl WindowManager {
already_moved_window_handles.remove(&window.hwnd);
}
WindowManagerEvent::FocusChange(_, window) => {
self.update_focused_workspace(self.mouse_follows_focus, false)?;
let workspace = self.focused_workspace_mut()?;
if !workspace
.floating_windows()
@@ -237,27 +239,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;
}
}
}
@@ -266,6 +288,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()
@@ -277,26 +301,45 @@ 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()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace.contains_window(window.hwnd) {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(*window);
self.update_focused_workspace(false)?;
if !workspace_contains_window && !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)?;
}
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(*window);
self.update_focused_workspace(true)?;
}
if workspace_contains_window {
let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container {
if let Some(monocle_window) = monocle.focused_window() {
if monocle_window.hwnd == window.hwnd {
monocle_window_event = true;
}
}
}
if !monocle_window_event && monocle_container.is_some() {
window.hide();
}
}
}
@@ -335,38 +378,53 @@ impl WindowManager {
let new_window_behaviour = self.window_container_behaviour;
let workspace = self.focused_workspace_mut()?;
if !workspace
.floating_windows()
.iter()
.any(|w| w.hwnd == window.hwnd)
{
let focused_container_idx = workspace.focused_container_idx();
let new_position = WindowsApi::window_rect(window.hwnd())?;
let focused_container_idx = workspace.focused_container_idx();
let new_position = WindowsApi::window_rect(window.hwnd())?;
let old_position = *workspace
.latest_layout()
.get(focused_container_idx)
// If the move was to another monitor with an empty workspace, the
// workspace here will refer to that empty workspace, which won't
// have any latest layout set. We fall back to a Default for Rect
// which allows us to make a reasonable guess that the drag has taken
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
let old_position = *workspace
.latest_layout()
.get(focused_container_idx)
// If the move was to another monitor with an empty workspace, the
// workspace here will refer to that empty workspace, which won't
// have any latest layout set. We fall back to a Default for Rect
// which allows us to make a reasonable guess that the drag has taken
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
if !moved_across_monitors {
// So we'll check if the origin monitor index and the target monitor index
// are different, if they are, we can set the override
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
if moved_across_monitors {
// Want to make sure that we exclude unmanaged windows from cross-monitor
// moves with a mouse, otherwise the currently focused idx container will
// be moved when we just want to drag an unmanaged window
let origin_workspace = self
.monitors()
.get(origin_monitor_idx)
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
.workspaces()
.get(origin_workspace_idx)
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
if let Some((origin_monitor_idx, _, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
if !moved_across_monitors {
// So we'll check if the origin monitor index and the target monitor index
// are different, if they are, we can set the override
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
let managed_window =
origin_workspace.contains_managed_window(window.hwnd);
if !managed_window {
moved_across_monitors = false;
}
}
}
}
let workspace = self.focused_workspace_mut()?;
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
let resize = Rect {
left: new_position.left - old_position.left,
top: new_position.top - old_position.top,
@@ -376,7 +434,11 @@ impl WindowManager {
// If we have moved across the monitors, use that override, otherwise determine
// if a move has taken place by ruling out a resize
let is_move = moved_across_monitors || resize.right == 0 && resize.bottom == 0;
let right_bottom_constant = 0;
let is_move = moved_across_monitors
|| resize.right.abs() == right_bottom_constant
&& resize.bottom.abs() == right_bottom_constant;
if is_move {
tracing::info!("moving with mouse");
@@ -424,11 +486,11 @@ impl WindowManager {
// the origin monitor's focused workspace
self.focus_monitor(origin_monitor_idx)?;
self.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
self.focus_monitor(target_monitor_idx)?;
self.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
// Here we handle a simple move on the same monitor which is treated as
// a container swap
@@ -439,11 +501,12 @@ impl WindowManager {
Some(target_idx) => {
workspace
.swap_containers(focused_container_idx, target_idx);
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
None => {
self.update_focused_workspace(
self.mouse_follows_focus,
false,
)?;
}
}
@@ -452,11 +515,12 @@ impl WindowManager {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
workspace.move_window_to_container(target_idx)?;
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
None => {
self.update_focused_workspace(
self.mouse_follows_focus,
false,
)?;
}
}
@@ -468,17 +532,17 @@ impl WindowManager {
let mut ops = vec![];
macro_rules! resize_op {
($coordinate:expr, $comparator:tt, $direction:expr) => {{
let adjusted = $coordinate * 2;
let sizing = if adjusted $comparator 0 {
Sizing::Decrease
} else {
Sizing::Increase
};
($coordinate:expr, $comparator:tt, $direction:expr) => {{
let adjusted = $coordinate * 2;
let sizing = if adjusted $comparator 0 {
Sizing::Decrease
} else {
Sizing::Increase
};
($direction, sizing, adjusted.abs())
}};
}
($direction, sizing, adjusted.abs())
}};
}
if resize.left != 0 {
ops.push(resize_op!(resize.left, >, OperationDirection::Left));
@@ -488,11 +552,14 @@ impl WindowManager {
ops.push(resize_op!(resize.top, >, OperationDirection::Up));
}
if resize.right != 0 && resize.left == 0 {
let top_left_constant = BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst);
if resize.right != 0 && resize.left == top_left_constant {
ops.push(resize_op!(resize.right, <, OperationDirection::Right));
}
if resize.bottom != 0 && resize.top == 0 {
if resize.bottom != 0 && resize.top == top_left_constant {
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
}
@@ -500,116 +567,20 @@ impl WindowManager {
self.resize_window(edge, sizing, delta, true)?;
}
self.update_focused_workspace(false)?;
self.update_focused_workspace(false, false)?;
}
}
}
WindowManagerEvent::DisplayChange(..)
| WindowManagerEvent::MouseCapture(..)
WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::Uncloak(..) => {}
| WindowManagerEvent::TitleUpdate(..) => {}
};
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() {
@@ -630,12 +601,26 @@ impl WindowManager {
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &known_hwnds)?;
notify_subscribers(&serde_json::to_string(&Notification {
event: NotificationEvent::WindowManager(*event),
state: self.as_ref().into(),
})?)?;
tracing::info!("processed: {}", event.window().to_string());
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)?;
stackbar_manager::event_tx().send(stackbar_manager::Notification)?;
// Too many spammy OBJECT_NAMECHANGE events from JetBrains IDEs
if !matches!(
event,
WindowManagerEvent::Show(WinEvent::ObjectNameChange, _)
) {
tracing::info!("processed: {}", event.window().to_string());
} else {
tracing::trace!("processed: {}", event.window().to_string());
}
Ok(())
}
}

66
komorebi/src/reaper.rs Normal file
View File

@@ -0,0 +1,66 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::WindowManager;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match find_orphans(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 find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("watching");
let arc = wm.clone();
loop {
std::thread::sleep(Duration::from_secs(1));
let mut wm = arc.lock();
let offset = wm.work_area_offset;
for (i, monitor) in wm.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 {
offset
};
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
border_manager::event_tx().send(border_manager::Notification)?;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
}
}
}
}

View File

@@ -0,0 +1,210 @@
mod stackbar;
use crate::container::Container;
use crate::stackbar_manager::stackbar::Stackbar;
use crate::WindowManager;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Foundation::HWND;
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
lazy_static! {
pub static ref STACKBAR_STATE: Mutex<HashMap<String, Stackbar>> = Mutex::new(HashMap::new());
static ref STACKBARS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref STACKBARS_CONTAINERS: Mutex<HashMap<isize, Container>> = 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 should_have_stackbar(window_count: usize) -> bool {
match STACKBAR_MODE.load() {
StackbarMode::Always => true,
StackbarMode::OnStack => window_count > 1,
StackbarMode::Never => false,
}
}
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();
'receiver: for _ in receiver {
let mut stackbars = STACKBAR_STATE.lock();
let mut stackbars_monitors = STACKBARS_MONITORS.lock();
// Check the wm state every time we receive a notification
let mut state = wm.lock();
// If stackbars are disabled
if matches!(STACKBAR_MODE.load(), StackbarMode::Never) {
for (_, stackbar) in stackbars.iter() {
stackbar.destroy()?;
}
stackbars.clear();
continue 'receiver;
}
for (monitor_idx, m) in state.monitors_mut().iter_mut().enumerate() {
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace_mut() {
// Workspaces with tiling disabled don't have stackbars
if !ws.tile() {
let mut to_remove = vec![];
for (id, border) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
border.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
continue 'receiver;
}
let is_maximized = WindowsApi::is_zoomed(HWND(
WindowsApi::foreground_window().unwrap_or_default(),
));
// Handle the monocle container separately
if ws.monocle_container().is_some() || is_maximized {
// Destroy any stackbars associated with the focused workspace
let mut to_remove = vec![];
for (id, stackbar) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
stackbar.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
continue 'receiver;
}
// Destroy any stackbars 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, stackbar) in stackbars.iter() {
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
{
stackbar.destroy()?;
to_remove.push(id.clone());
}
}
for id in &to_remove {
stackbars.remove(id);
}
let container_padding = ws
.container_padding()
.unwrap_or_else(|| DEFAULT_CONTAINER_PADDING.load_consume());
'containers: for container in ws.containers_mut() {
let should_add_stackbar = match STACKBAR_MODE.load() {
StackbarMode::Always => true,
StackbarMode::OnStack => container.windows().len() > 1,
StackbarMode::Never => false,
};
if !should_add_stackbar {
if let Some(stackbar) = stackbars.get(container.id()) {
stackbar.destroy()?
}
stackbars.remove(container.id());
stackbars_monitors.remove(container.id());
continue 'containers;
}
// Get the stackbar entry for this container from the map or create one
let stackbar = match stackbars.entry(container.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(stackbar) = Stackbar::create(container.id()) {
entry.insert(stackbar)
} else {
continue 'receiver;
}
}
};
stackbars_monitors.insert(container.id().clone(), monitor_idx);
let rect = WindowsApi::window_rect(
container
.focused_window()
.copied()
.unwrap_or_default()
.hwnd(),
)?;
stackbar.update(container_padding, container, &rect)?;
}
}
}
}
Ok(())
}

View File

@@ -0,0 +1,328 @@
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::STYLE;
use crate::container::Container;
use crate::stackbar_manager::STACKBARS_CONTAINERS;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::WINDOWS_11;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_core::BorderStyle;
use komorebi_core::Rect;
use komorebi_core::StackbarLabel;
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
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::CreateFontIndirectW;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::DrawTextW;
use windows::Win32::Graphics::Gdi::GetDC;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::ReleaseDC;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::SetBkColor;
use windows::Win32::Graphics::Gdi::SetTextColor;
use windows::Win32::Graphics::Gdi::DT_CENTER;
use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS;
use windows::Win32::Graphics::Gdi::DT_SINGLELINE;
use windows::Win32::Graphics::Gdi::DT_VCENTER;
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
use windows::Win32::Graphics::Gdi::FW_BOLD;
use windows::Win32::Graphics::Gdi::LOGFONTW;
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
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::SetLayeredWindowAttributes;
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::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
#[derive(Debug)]
pub struct Stackbar {
pub hwnd: isize,
}
impl From<isize> for Stackbar {
fn from(value: isize) -> Self {
Self { hwnd: value }
}
}
impl Stackbar {
pub const fn hwnd(&self) -> HWND {
HWND(self.hwnd)
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komostackbar-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
let h_module = WindowsApi::module_handle_w()?;
let window_class = WNDCLASSW {
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hInstance: h_module.into(),
lpszClassName: class_name,
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
};
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let name_cl = name.clone();
std::thread::spawn(move || -> color_eyre::Result<()> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
PCWSTR(name_cl.as_ptr()),
PCWSTR(name_cl.as_ptr()),
WS_POPUP | WS_VISIBLE,
0,
0,
0,
0,
None,
None,
h_module,
None,
);
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));
}
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?.0,
})
}
pub fn destroy(&self) -> color_eyre::Result<()> {
WindowsApi::close_window(self.hwnd())
}
pub fn update(
&self,
container_padding: i32,
container: &mut Container,
layout: &Rect,
) -> color_eyre::Result<()> {
let width = STACKBAR_TAB_WIDTH.load_consume();
let height = STACKBAR_TAB_HEIGHT.load_consume();
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
let background = STACKBAR_TAB_BACKGROUND_COLOUR.load_consume();
let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load_consume();
let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load_consume();
let mut stackbars_containers = STACKBARS_CONTAINERS.lock();
stackbars_containers.insert(self.hwnd, container.clone());
let mut layout = *layout;
let workspace_specific_offset =
BORDER_WIDTH.load_consume() + BORDER_OFFSET.load_consume() + container_padding;
layout.top -= workspace_specific_offset + STACKBAR_TAB_HEIGHT.load_consume();
layout.left -= workspace_specific_offset;
WindowsApi::position_window(self.hwnd(), &layout, false)?;
unsafe {
let hdc = GetDC(self.hwnd());
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
let hbrush = CreateSolidBrush(COLORREF(background));
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
SetBkColor(hdc, COLORREF(background));
let hfont = CreateFontIndirectW(&LOGFONTW {
lfWeight: FW_BOLD.0 as i32,
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
..Default::default()
});
SelectObject(hdc, hfont);
for (i, window) in container.windows().iter().enumerate() {
if window.hwnd == container.focused_window().copied().unwrap_or_default().hwnd {
SetTextColor(hdc, COLORREF(focused_text_colour));
} else {
SetTextColor(hdc, COLORREF(unfocused_text_colour));
}
let left = gap + (i as i32 * (width + gap));
let mut rect = Rect {
top: 0,
left,
right: left + width,
bottom: height,
};
match *STYLE.lock() {
BorderStyle::System => {
if *WINDOWS_11 {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
} else {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
BorderStyle::Rounded => {
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
}
BorderStyle::Square => {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
let label = match STACKBAR_LABEL.load() {
StackbarLabel::Process => {
let exe = window.exe()?;
exe.trim_end_matches(".exe").to_string()
}
StackbarLabel::Title => window.title()?,
};
let mut tab_title: Vec<u16> = label.encode_utf16().collect();
rect.left_padding(10);
rect.right_padding(10);
DrawTextW(
hdc,
&mut tab_title,
&mut rect.into(),
DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS,
);
}
ReleaseDC(self.hwnd(), hdc);
}
Ok(())
}
pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect {
Rect {
bottom: STACKBAR_TAB_HEIGHT.load_consume(),
..*layout
}
}
unsafe extern "system" fn callback(
hwnd: HWND,
msg: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
unsafe {
match msg {
WM_LBUTTONDOWN => {
let stackbars_containers = STACKBARS_CONTAINERS.lock();
if let Some(container) = stackbars_containers.get(&hwnd.0) {
let x = l_param.0 as i32 & 0xFFFF;
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
let width = STACKBAR_TAB_WIDTH.load_consume();
let height = STACKBAR_TAB_HEIGHT.load_consume();
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
let focused_window_idx = container.focused_window_idx();
let focused_window_rect = WindowsApi::window_rect(
container
.focused_window()
.cloned()
.unwrap_or_default()
.hwnd(),
)
.unwrap_or_default();
for (index, window) in container.windows().iter().enumerate() {
let left = gap + (index as i32 * (width + gap));
let right = left + width;
let top = 0;
let bottom = height;
if x >= left && x <= right && y >= top && y <= bottom {
// If we are focusing a window that isn't currently focused in the
// stackbar, make sure we update its location so that it doesn't render
// on top of other tiles before eventually ending up in the correct
// tile
if index != focused_window_idx {
if let Err(err) =
window.set_position(&focused_window_rect, false)
{
tracing::error!(
"stackbar WM_LBUTTONDOWN repositioning error: hwnd {} ({})",
*window,
err
);
}
}
// Restore the window corresponding to the tab we have clicked
window.restore();
if let Err(err) = window.focus(false) {
tracing::error!(
"stackbar WMLBUTTONDOWN focus error: hwnd {} ({})",
*window,
err
);
}
} else {
// Hide any windows in the stack that don't correspond to the window
// we have clicked
window.hide();
}
}
}
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, w_param, l_param),
}
}
}
}

View File

@@ -1,21 +1,23 @@
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;
use crate::monitor_reconciliator;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
use crate::stackbar_manager::STACKBAR_MODE;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
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_OVERFLOW_IDENTIFIERS;
use crate::BORDER_WIDTH;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
@@ -29,17 +31,22 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_RULES;
use komorebi_core::StackbarLabel;
use komorebi_core::StackbarMode;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::notify::DebouncedEvent;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_core::config_generation::ApplicationConfiguration;
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
use komorebi_core::config_generation::ApplicationOptions;
use komorebi_core::config_generation::IdWithIdentifier;
use komorebi_core::config_generation::MatchingRule;
use komorebi_core::config_generation::MatchingStrategy;
use komorebi_core::resolve_home_path;
use komorebi_core::ApplicationIdentifier;
use komorebi_core::BorderStyle;
use komorebi_core::DefaultLayout;
use komorebi_core::FocusFollowsMouseImplementation;
use komorebi_core::HidingBehaviour;
@@ -65,16 +72,18 @@ use uds_windows::UnixListener;
use uds_windows::UnixStream;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ActiveWindowBorderColours {
pub struct BorderColours {
/// 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)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceConfig {
/// Name
pub name: String,
@@ -188,13 +197,19 @@ impl From<&Workspace> for WorkspaceConfig {
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Workspace configurations
pub workspaces: Vec<WorkspaceConfig>,
/// 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 {
@@ -207,12 +222,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.20`
/// The `komorebi.json` static configuration file reference for `v0.1.26`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -248,10 +265,19 @@ pub struct StaticConfig {
pub border_offset: Option<i32>,
/// Display an active window border (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border: Option<bool>,
#[serde(alias = "active_window_border")]
pub border: Option<bool>,
/// Active window border colours for different container types
#[serde(skip_serializing_if = "Option::is_none")]
pub active_window_border_colours: Option<ActiveWindowBorderColours>,
#[serde(alias = "active_window_border_colours")]
pub border_colours: Option<BorderColours>,
/// Active window border style (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_style")]
pub border_style: Option<BorderStyle>,
/// Active window border z-order (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>,
/// Global default workspace padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
pub default_workspace_padding: Option<i32>,
@@ -269,28 +295,96 @@ pub struct StaticConfig {
pub global_work_area_offset: Option<Rect>,
/// Individual window floating rules
#[serde(skip_serializing_if = "Option::is_none")]
pub float_rules: Option<Vec<IdWithIdentifier>>,
pub float_rules: Option<Vec<MatchingRule>>,
/// Individual window force-manage rules
#[serde(skip_serializing_if = "Option::is_none")]
pub manage_rules: Option<Vec<IdWithIdentifier>>,
pub manage_rules: Option<Vec<MatchingRule>>,
/// Identify border overflow applications
#[serde(skip_serializing_if = "Option::is_none")]
pub border_overflow_applications: Option<Vec<IdWithIdentifier>>,
pub border_overflow_applications: Option<Vec<MatchingRule>>,
/// Identify tray and multi-window applications
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_and_multi_window_applications: Option<Vec<IdWithIdentifier>>,
pub tray_and_multi_window_applications: Option<Vec<MatchingRule>>,
/// Identify applications that have the WS_EX_LAYERED extended window style
#[serde(skip_serializing_if = "Option::is_none")]
pub layered_applications: Option<Vec<IdWithIdentifier>>,
pub layered_applications: Option<Vec<MatchingRule>>,
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change_applications: Option<Vec<IdWithIdentifier>>,
pub object_name_change_applications: Option<Vec<MatchingRule>>,
/// Set monitor index preferences
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
/// 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>,
}
impl StaticConfig {
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("border", ["active_window_border"]);
map.insert("border_width", ["active_window_border_width"]);
map.insert("border_offset", ["active_window_border_offset"]);
map.insert("border_colours", ["active_window_border_colours"]);
map.insert("border_style", ["active_window_border_style"]);
let mut display = false;
for (_, aliases) in &map {
for a in aliases {
if raw.contains(a) {
display = true;
}
}
}
if display {
println!("\nYour configuration file contains some options that have been renamed or deprecated:\n");
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
println!(r#""{alias}" is now "{canonical}""#);
}
}
}
}
}
pub fn deprecated(raw: &str) {
let deprecated = ["invisible_borders"];
for option in deprecated {
if raw.contains(option) {
println!(r#""{option}" is deprecated and can be removed"#);
}
}
}
}
#[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 height
pub label: Option<StackbarLabel>,
/// Stackbar mode
pub mode: Option<StackbarMode>,
/// Stackbar tab configuration options
pub tabs: Option<TabsConfig>,
}
impl From<&WindowManager> for StaticConfig {
@@ -346,21 +440,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)
}),
Option::from(BorderColours {
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),
)),
})
};
@@ -375,10 +464,12 @@ 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)),
active_window_border_colours: border_colours,
border_width: Option::from(border_manager::BORDER_WIDTH.load(Ordering::SeqCst)),
border_offset: Option::from(border_manager::BORDER_OFFSET.load(Ordering::SeqCst)),
border: Option::from(border_manager::BORDER_ENABLED.load(Ordering::SeqCst)),
border_colours,
border_style: Option::from(*STYLE.lock()),
border_z_order: Option::from(*Z_ORDER.lock()),
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
),
@@ -396,6 +487,7 @@ impl From<&WindowManager> for StaticConfig {
object_name_change_applications: None,
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
stackbar: None,
}
}
}
@@ -426,130 +518,97 @@ 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(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(enabled) = &self.border {
border_manager::BORDER_ENABLED.store(*enabled, Ordering::SeqCst);
}
if let Some(colours) = &self.border_colours {
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 border_style = self.border_style.unwrap_or_default();
*STYLE.lock() = border_style;
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
let mut border_overflow_identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
let mut layered_identifiers = LAYERED_WHITELIST.lock();
if let Some(float) = &mut self.float_rules {
for identifier in float {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !float_identifiers.contains(identifier) {
float_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
}
}
if let Some(rules) = &mut self.float_rules {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
}
if let Some(manage) = &mut self.manage_rules {
for identifier in manage {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !manage_identifiers.contains(identifier) {
manage_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
}
}
if let Some(rules) = &mut self.manage_rules {
populate_rules(rules, &mut manage_identifiers, &mut regex_identifiers)?;
}
if let Some(identifiers) = &mut self.object_name_change_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !object_name_change_identifiers.contains(identifier) {
object_name_change_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
}
}
if let Some(rules) = &mut self.object_name_change_applications {
populate_rules(
rules,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(identifiers) = &mut self.layered_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !layered_identifiers.contains(identifier) {
layered_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
}
}
if let Some(rules) = &mut self.layered_applications {
populate_rules(rules, &mut layered_identifiers, &mut regex_identifiers)?;
}
if let Some(identifiers) = &mut self.border_overflow_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if !border_overflow_identifiers.contains(identifier) {
border_overflow_identifiers.push(identifier.clone());
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
}
}
if let Some(rules) = &mut self.tray_and_multi_window_applications {
populate_rules(
rules,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(identifiers) = &mut self.tray_and_multi_window_applications {
for identifier in identifiers {
if identifier.matching_strategy.is_none() {
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
if let Some(stackbar) = &self.stackbar {
if let Some(height) = &stackbar.height {
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
}
if let Some(label) = &stackbar.label {
STACKBAR_LABEL.store(*label);
}
if let Some(mode) = &stackbar.mode {
STACKBAR_MODE.store(*mode);
}
if let Some(tabs) = &stackbar.tabs {
if let Some(background) = &tabs.background {
STACKBAR_TAB_BACKGROUND_COLOUR.store((*background).into(), Ordering::SeqCst);
}
if !tray_and_multi_window_identifiers.contains(identifier) {
tray_and_multi_window_identifiers.push(identifier.clone());
if let Some(colour) = &tabs.focused_text {
STACKBAR_FOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst);
}
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&identifier.id)?;
regex_identifiers.insert(identifier.id.clone(), re);
}
if let Some(colour) = &tabs.unfocused_text {
STACKBAR_UNFOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst);
}
if let Some(width) = &tabs.width {
STACKBAR_TAB_WIDTH.store(*width, Ordering::SeqCst);
}
}
}
@@ -560,121 +619,43 @@ impl StaticConfig {
let asc = ApplicationConfigurationGenerator::load(&content)?;
for mut entry in asc {
if let Some(float) = entry.float_identifiers {
for f in float {
let mut without_comment: IdWithIdentifier = f.into();
if without_comment.matching_strategy.is_none() {
without_comment.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !float_identifiers.contains(&without_comment) {
float_identifiers.push(without_comment.clone());
if matches!(
without_comment.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&without_comment.id)?;
regex_identifiers.insert(without_comment.id.clone(), re);
}
}
}
if let Some(rules) = &mut entry.float_identifiers {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
}
if let Some(options) = entry.options {
if let Some(ref options) = entry.options {
let options = options.clone();
for o in options {
match o {
ApplicationOptions::ObjectNameChange => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !object_name_change_identifiers.contains(&entry.identifier) {
object_name_change_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
}
populate_option(
&mut entry,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::Layered => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !layered_identifiers.contains(&entry.identifier) {
layered_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
}
}
ApplicationOptions::BorderOverflow => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !border_overflow_identifiers.contains(&entry.identifier) {
border_overflow_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
}
populate_option(
&mut entry,
&mut layered_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::TrayAndMultiWindow => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !tray_and_multi_window_identifiers.contains(&entry.identifier) {
tray_and_multi_window_identifiers
.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
}
populate_option(
&mut entry,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::Force => {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy =
Option::from(MatchingStrategy::Legacy);
}
if !manage_identifiers.contains(&entry.identifier) {
manage_identifiers.push(entry.identifier.clone());
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
}
populate_option(
&mut entry,
&mut manage_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::BorderOverflow => {} // deprecated
}
}
}
@@ -710,7 +691,6 @@ impl StaticConfig {
let mut wm = WindowManager {
monitors: Ring::default(),
monitor_cache: HashMap::new(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
@@ -744,10 +724,10 @@ impl StaticConfig {
let bytes = SocketMessage::ReloadStaticConfiguration(path.clone()).as_bytes()?;
wm.hotwatch.watch(path, move |event| match event {
wm.hotwatch.watch(path, move |event| match event.kind {
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
// a NoticeRemove, presumably because of the use of swap files?
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {
EventKind::Modify(_) | EventKind::Remove(_) => {
let socket = DATA_DIR.join("komorebi.sock");
let mut stream =
UnixStream::connect(socket).expect("could not connect to komorebi.sock");
@@ -768,9 +748,20 @@ impl StaticConfig {
if let Some(monitors) = value.monitors {
for (i, monitor) in monitors.iter().enumerate() {
{
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
if let Some(device_id) = display_index_preferences.get(&i) {
monitor_reconciliator::insert_in_monitor_cache(device_id, monitor.clone());
}
}
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(
@@ -798,13 +789,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()?;
if value.border == Some(true) {
border_manager::BORDER_ENABLED.store(true, Ordering::SeqCst);
}
Ok(())
@@ -821,6 +807,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(
@@ -848,16 +838,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.border {
border_manager::BORDER_ENABLED.store(enabled, Ordering::SeqCst);
}
if let Some(val) = value.window_container_behaviour {
@@ -901,3 +883,67 @@ impl StaticConfig {
Ok(())
}
}
fn populate_option(
entry: &mut ApplicationConfiguration,
identifiers: &mut Vec<MatchingRule>,
regex_identifiers: &mut HashMap<String, Regex>,
) -> Result<()> {
if entry.identifier.matching_strategy.is_none() {
entry.identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
let rule = MatchingRule::Simple(entry.identifier.clone());
if !identifiers.contains(&rule) {
identifiers.push(rule);
if matches!(
entry.identifier.matching_strategy,
Some(MatchingStrategy::Regex)
) {
let re = Regex::new(&entry.identifier.id)?;
regex_identifiers.insert(entry.identifier.id.clone(), re);
}
}
Ok(())
}
fn populate_rules(
matching_rules: &mut Vec<MatchingRule>,
identifiers: &mut Vec<MatchingRule>,
regex_identifiers: &mut HashMap<String, Regex>,
) -> Result<()> {
for matching_rule in matching_rules {
if !identifiers.contains(matching_rule) {
match matching_rule {
MatchingRule::Simple(simple) => {
if simple.matching_strategy.is_none() {
simple.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if matches!(simple.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&simple.id)?;
regex_identifiers.insert(simple.id.clone(), re);
}
}
MatchingRule::Composite(composite) => {
for rule in composite {
if rule.matching_strategy.is_none() {
rule.matching_strategy = Option::from(MatchingStrategy::Legacy);
}
if matches!(rule.matching_strategy, Some(MatchingStrategy::Regex)) {
let re = Regex::new(&rule.id)?;
regex_identifiers.insert(rule.id.clone(), re);
}
}
}
}
identifiers.push(matching_rule.clone());
}
}
Ok(())
}

View File

@@ -1,4 +1,6 @@
use bitflags::bitflags;
use serde::Deserialize;
use serde::Serialize;
use windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
use windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
@@ -56,7 +58,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
bitflags! {
#[derive(Default)]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
pub struct WindowStyle: u32 {
const BORDER = WS_BORDER.0;
const CAPTION = WS_CAPTION.0;
@@ -90,7 +92,7 @@ bitflags! {
// https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
bitflags! {
#[derive(Default)]
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
pub struct ExtendedWindowStyle: u32 {
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
const APPWINDOW = WS_EX_APPWINDOW.0;

View File

@@ -4,15 +4,16 @@ use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Write as _;
use std::time::Duration;
use color_eyre::eyre;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use komorebi_core::config_generation::IdWithIdentifier;
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;
@@ -37,9 +38,9 @@ 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(crate) hwnd: isize,
pub hwnd: isize,
}
#[allow(clippy::module_name_repetitions)]
@@ -95,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()
}
@@ -123,7 +123,7 @@ impl Window {
HWND(self.hwnd)
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
pub fn center(&self, work_area: &Rect) -> Result<()> {
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
@@ -138,7 +138,11 @@ impl Window {
)
}
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
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)
}
@@ -151,6 +155,10 @@ impl Window {
WindowsApi::is_iconic(self.hwnd())
}
pub fn is_visible(self) -> bool {
WindowsApi::is_window_visible(self.hwnd())
}
pub fn hide(self) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
@@ -215,129 +223,26 @@ impl Window {
WindowsApi::unmaximize_window(self.hwnd());
}
pub fn raise(self) {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
let current_thread_id = WindowsApi::current_thread_id();
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
// hook has been installed
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not attach to window thread input processing mechanism, but continuing execution of raise(): {}",
error
);
}
};
// Raise Window to foreground
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not set as foreground window, but continuing execution of raise(): {}",
error
);
}
};
// This isn't really needed when the above command works as expected via AHK
match WindowsApi::set_focus(self.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not set focus, but continuing execution of raise(): {}",
error
);
}
};
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, false) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not detach from window thread input processing mechanism, but continuing execution of raise(): {}",
error
);
}
};
}
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
// Attach komorebi thread to Window thread
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
let current_thread_id = WindowsApi::current_thread_id();
// If the target window is already focused, do nothing.
if let Ok(ihwnd) = WindowsApi::foreground_window() {
if HWND(ihwnd) == self.hwnd() {
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
}
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
// hook has been installed
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not attach to window thread input processing mechanism, but continuing execution of focus(): {}",
error
);
return Ok(());
}
};
// Raise Window to foreground
let mut foregrounded = false;
let mut tried_resetting_foreground_access = false;
let mut max_attempts = 10;
while !foregrounded && max_attempts > 0 {
match WindowsApi::set_foreground_window(self.hwnd()) {
Ok(()) => {
foregrounded = true;
}
Err(error) => {
max_attempts -= 1;
tracing::error!(
"could not set as foreground window, but continuing execution of focus(): {}",
error
);
// If this still doesn't work then maybe try https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-locksetforegroundwindow
if !tried_resetting_foreground_access {
let process_id = WindowsApi::current_process_id();
if WindowsApi::allow_set_foreground_window(process_id).is_ok() {
tried_resetting_foreground_access = true;
}
}
}
};
}
WindowsApi::raise_and_focus_window(self.hwnd())?;
// Center cursor in Window
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
}
// This isn't really needed when the above command works as expected via AHK
match WindowsApi::set_focus(self.hwnd()) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not set focus, but continuing execution of focus(): {}",
error
);
}
};
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, false) {
Ok(()) => {}
Err(error) => {
tracing::error!(
"could not detach from window thread input processing mechanism, but continuing execution of focus(): {}",
error
);
}
};
Ok(())
}
@@ -377,6 +282,14 @@ impl Window {
WindowsApi::window_text_w(self.hwnd())
}
pub fn path(self) -> Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
let handle = WindowsApi::process_handle(process_id)?;
let path = WindowsApi::exe_path(handle);
WindowsApi::close_process(handle)?;
path
}
pub fn exe(self) -> Result<String> {
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
let handle = WindowsApi::process_handle(process_id)?;
@@ -411,18 +324,27 @@ impl Window {
self.update_style(&style)
}
#[tracing::instrument(fields(exe, title))]
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
if let Some(WindowManagerEvent::DisplayChange(_)) = event {
return Ok(true);
#[tracing::instrument(fields(exe, title), skip(debug))]
pub fn should_manage(
self,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> Result<bool> {
if !self.is_window() {
return Ok(false);
}
#[allow(clippy::question_mark)]
debug.is_window = true;
if self.title().is_err() {
return Ok(false);
}
let is_cloaked = self.is_cloaked()?;
debug.has_title = true;
let is_cloaked = self.is_cloaked().unwrap_or_default();
debug.is_cloaked = is_cloaked;
let mut allow_cloaked = false;
@@ -435,13 +357,28 @@ impl Window {
}
}
debug.allow_cloaked = allow_cloaked;
match (allow_cloaked, is_cloaked) {
// If allowing cloaked windows, we don't need to check the cloaked status
(true, _) |
// If not allowing cloaked windows, we need to ensure the window is not cloaked
(false, false) => {
if let (Ok(title), Ok(exe_name), Ok(class)) = (self.title(), self.exe(), self.class()) {
return Ok(window_is_eligible(&title, &exe_name, &class, &self.style()?, &self.ex_style()?, event));
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (self.title(), self.exe(), self.class(), self.path()) {
debug.title = Some(title.clone());
debug.exe_name = Some(exe_name.clone());
debug.class = Some(class.clone());
debug.path = Some(path.clone());
// calls for styles can fail quite often for events with windows that aren't really "windows"
// since we have moved up calls of should_manage to the beginning of the process_event handler,
// we should handle failures here gracefully to be able to continue the execution of process_event
if let (Ok(style), Ok(ex_style)) = (&self.style(), &self.ex_style()) {
debug.window_style = Some(*style);
debug.extended_window_style = Some(*ex_style);
let eligible = window_is_eligible(&title, &exe_name, &class, &path, style, ex_style, event, debug);
debug.should_manage = eligible;
return Ok(eligible);
}
}
}
_ => {}
@@ -451,17 +388,42 @@ impl Window {
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct RuleDebug {
pub should_manage: bool,
pub is_window: bool,
pub has_title: bool,
pub is_cloaked: bool,
pub allow_cloaked: bool,
pub window_style: Option<WindowStyle>,
pub extended_window_style: Option<ExtendedWindowStyle>,
pub title: Option<String>,
pub exe_name: Option<String>,
pub class: Option<String>,
pub path: Option<String>,
pub matches_permaignore_class: Option<String>,
pub matches_float_identifier: Option<MatchingRule>,
pub matches_managed_override: Option<MatchingRule>,
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
pub matches_no_titlebar: Option<String>,
}
#[allow(clippy::too_many_arguments)]
fn window_is_eligible(
title: &String,
exe_name: &String,
class: &String,
path: &str,
style: &WindowStyle,
ex_style: &ExtendedWindowStyle,
event: Option<WindowManagerEvent>,
debug: &mut RuleDebug,
) -> bool {
{
let permaignore_classes = PERMAIGNORE_CLASSES.lock();
if permaignore_classes.contains(class) {
debug.matches_permaignore_class = Some(class.clone());
return false;
}
}
@@ -469,42 +431,65 @@ fn window_is_eligible(
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let float_identifiers = FLOAT_IDENTIFIERS.lock();
let should_float = should_act(
let should_float = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&float_identifiers,
&regex_identifiers,
);
) {
debug.matches_float_identifier = Some(rule);
true
} else {
false
};
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
let managed_override = should_act(
let managed_override = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&manage_identifiers,
&regex_identifiers,
);
) {
debug.matches_managed_override = Some(rule);
true
} else {
false
};
if should_float && !managed_override {
return false;
}
let layered_whitelist = LAYERED_WHITELIST.lock();
let allow_layered = should_act(
let allow_layered = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&layered_whitelist,
&regex_identifiers,
);
) {
debug.matches_layered_whitelist = Some(rule);
true
} else {
false
};
// TODO: might need this for transparency
// let allow_layered = true;
let allow_wsl2_gui = {
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
wsl2_ui_processes.contains(exe_name)
let allow = wsl2_ui_processes.contains(exe_name);
if allow {
debug.matches_wsl2_gui = Some(exe_name.clone())
}
allow
};
let allow_titlebar_removed = {
@@ -512,6 +497,10 @@ fn window_is_eligible(
titlebars_removed.contains(exe_name)
};
if exe_name.contains("firefox") {
std::thread::sleep(Duration::from_millis(10));
}
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
// Get a lot of dupe events coming through that make the redrawing go crazy
@@ -522,8 +511,13 @@ fn window_is_eligible(
|| managed_override
{
return true;
} else if event.is_some() {
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
} else if let Some(event) = event {
tracing::debug!(
"ignoring (exe: {}, title: {}, event: {})",
exe_name,
title,
event
);
}
false
@@ -534,124 +528,290 @@ pub fn should_act(
title: &str,
exe_name: &str,
class: &str,
identifiers: &[IdWithIdentifier],
path: &str,
identifiers: &[MatchingRule],
regex_identifiers: &HashMap<String, Regex>,
) -> Option<MatchingRule> {
let mut matching_rule = None;
for rule in identifiers {
match rule {
MatchingRule::Simple(identifier) => {
if should_act_individual(
title,
exe_name,
class,
path,
identifier,
regex_identifiers,
) {
matching_rule = Some(rule.clone());
};
}
MatchingRule::Composite(identifiers) => {
let mut composite_results = vec![];
for identifier in identifiers {
composite_results.push(should_act_individual(
title,
exe_name,
class,
path,
identifier,
regex_identifiers,
));
}
if composite_results.iter().all(|&x| x) {
matching_rule = Some(rule.clone());
}
}
}
}
matching_rule
}
pub fn should_act_individual(
title: &str,
exe_name: &str,
class: &str,
path: &str,
identifier: &IdWithIdentifier,
regex_identifiers: &HashMap<String, Regex>,
) -> bool {
let mut should_act = false;
for identifier in identifiers {
match identifier.matching_strategy {
None => {
panic!("there is no matching strategy identified for this rule");
}
Some(MatchingStrategy::Legacy) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Equals) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::StartsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.starts_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::EndsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.ends_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Contains) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.contains(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Regex) => match identifier.kind {
ApplicationIdentifier::Title => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(title) {
should_act = true;
}
}
}
ApplicationIdentifier::Class => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(class) {
should_act = true;
}
}
}
ApplicationIdentifier::Exe => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(exe_name) {
should_act = true;
}
}
}
},
match identifier.matching_strategy {
None => {
panic!("there is no matching strategy identified for this rule");
}
Some(MatchingStrategy::Legacy) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Equals) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotEqual) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.eq(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.eq(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::StartsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.starts_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotStartWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.starts_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.starts_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::EndsWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.ends_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotEndWith) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.ends_with(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.ends_with(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Contains) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if class.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if exe_name.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if path.contains(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::DoesNotContain) => match identifier.kind {
ApplicationIdentifier::Title => {
if !title.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Class => {
if !class.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Exe => {
if !exe_name.contains(&identifier.id) {
should_act = true;
}
}
ApplicationIdentifier::Path => {
if !path.contains(&identifier.id) {
should_act = true;
}
}
},
Some(MatchingStrategy::Regex) => match identifier.kind {
ApplicationIdentifier::Title => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(title) {
should_act = true;
}
}
}
ApplicationIdentifier::Class => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(class) {
should_act = true;
}
}
}
ApplicationIdentifier::Exe => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(exe_name) {
should_act = true;
}
}
}
ApplicationIdentifier::Path => {
if let Some(re) = regex_identifiers.get(&identifier.id) {
if re.is_match(path) {
should_act = true;
}
}
}
},
}
should_act

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ pub enum WindowManagerEvent {
Manage(Window),
Unmanage(Window),
Raise(Window),
DisplayChange(Window),
TitleUpdate(WinEvent, Window),
}
impl Display for WindowManagerEvent {
@@ -75,8 +75,8 @@ impl Display for WindowManagerEvent {
Self::Raise(window) => {
write!(f, "Raise (Window: {window})")
}
Self::DisplayChange(window) => {
write!(f, "DisplayChange (Window: {window})")
Self::TitleUpdate(winevent, window) => {
write!(f, "TitleUpdate (WinEvent: {winevent}, Window: {window})")
}
}
}
@@ -97,8 +97,8 @@ impl WindowManagerEvent {
| Self::MouseCapture(_, window)
| Self::Raise(window)
| Self::Manage(window)
| Self::DisplayChange(window)
| Self::Unmanage(window) => window,
| Self::Unmanage(window)
| Self::TitleUpdate(_, window) => window,
}
}
@@ -139,19 +139,22 @@ impl WindowManagerEvent {
let title = &window.title().ok()?;
let exe_name = &window.exe().ok()?;
let class = &window.class().ok()?;
let path = &window.path().ok()?;
let should_trigger = should_act(
let should_trigger_show = should_act(
title,
exe_name,
class,
path,
&object_name_change_on_launch,
&regex_identifiers,
);
)
.is_some();
if should_trigger {
if should_trigger_show {
Option::from(Self::Show(winevent, window))
} else {
None
Option::from(Self::TitleUpdate(winevent, window))
}
}
_ => None,

View File

@@ -1,12 +1,12 @@
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::ffi::c_void;
use std::sync::atomic::Ordering;
use std::mem::size_of;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::eyre::Error;
use color_eyre::Result;
use widestring::U16CStr;
use windows::core::Result as WindowsCrateResult;
use windows::core::PCWSTR;
use windows::core::PWSTR;
@@ -30,13 +30,12 @@ use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
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::DISPLAY_DEVICEW;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::HBRUSH;
use windows::Win32::Graphics::Gdi::HDC;
use windows::Win32::Graphics::Gdi::HMONITOR;
@@ -45,9 +44,8 @@ use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
use windows::Win32::System::Threading::AttachThreadInput;
use windows::Win32::System::RemoteDesktop::WTSRegisterSessionNotification;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::System::Threading::OpenProcess;
use windows::Win32::System::Threading::QueryFullProcessImageNameW;
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
@@ -59,13 +57,13 @@ use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::HiDpi::MDT_EFFECTIVE_DPI;
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState;
use windows::Win32::UI::Input::KeyboardAndMouse::SendInput;
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0;
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_MOUSE;
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTDOWN;
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP;
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT;
use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON;
use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU;
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop;
@@ -96,12 +94,9 @@ use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
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_NOTOPMOST;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
@@ -112,6 +107,9 @@ use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SPI_GETFOREGROUNDLOCKTIMEOUT;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETFOREGROUNDLOCKTIMEOUT;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
@@ -139,8 +137,9 @@ 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;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
pub enum WindowsResult<T, E> {
Err(E),
@@ -221,58 +220,74 @@ impl WindowsApi {
}
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
let mut monitors: Vec<(String, isize)> = vec![];
let monitors_ref: &mut Vec<(String, isize)> = monitors.as_mut();
Self::enum_display_monitors(
Some(windows_callbacks::valid_display_monitors),
monitors_ref as *mut Vec<(String, isize)> as isize,
)?;
Ok(win32_display_data::connected_displays()
.flatten()
.map(|d| {
let name = d.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
Ok(monitors)
(name, d.hmonitor)
})
.collect::<Vec<_>>())
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
Self::enum_display_monitors(
Some(windows_callbacks::enum_display_monitor),
monitors as *mut Ring<Monitor> as isize,
)?;
'read: for display in win32_display_data::connected_displays().flatten() {
let path = display.device_path.clone();
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
Ok(())
}
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
pub fn enum_display_devices(
index: u32,
lp_device: Option<*const u16>,
) -> Result<DISPLAY_DEVICEW> {
#[allow(clippy::option_if_let_else)]
let lp_device = match lp_device {
None => PCWSTR::null(),
Some(lp_device) => PCWSTR(lp_device),
};
for monitor in monitors.elements() {
if device_id.eq(monitor.device_id()) {
continue 'read;
}
}
let mut display_device = DISPLAY_DEVICEW {
cb: u32::try_from(std::mem::size_of::<DISPLAY_DEVICEW>())?,
..Default::default()
};
let m = monitor::new(
display.hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
);
match unsafe {
EnumDisplayDevicesW(
lp_device,
index,
std::ptr::addr_of_mut!(display_device),
EDD_GET_DEVICE_INTERFACE_NAME,
)
}
.ok()
{
Ok(()) => {}
Err(error) => {
tracing::error!("enum_display_devices: {}", error);
return Err(error.into());
let mut index_preference = None;
let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
for (index, monitor_size) in &*monitor_index_preferences {
if m.size() == monitor_size {
index_preference = Option::from(index);
}
}
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, id) in &*display_index_preferences {
if id.eq(m.device_id()) {
index_preference = Option::from(index);
}
}
if monitors.elements().is_empty() {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
let current_len = monitors.elements().len();
while *preference > current_len {
monitors.elements_mut().reserve(1);
}
monitors.elements_mut().insert(*preference, m);
} else {
monitors.elements_mut().push_back(m);
}
}
Ok(display_device)
Ok(())
}
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
@@ -344,12 +359,19 @@ impl WindowsApi {
/// the layout to account for any window shadow borders (the window painted
/// region will match layout on completion).
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
let flags = SetWindowPosition::NO_ACTIVATE
let mut flags = SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_COPY_BITS
| SetWindowPosition::FRAME_CHANGED;
let shadow_rect = Self::shadow_rect(hwnd)?;
// If the request is to place the window on top, then HWND_TOP will take
// effect, otherwise pass NO_Z_ORDER that will cause set_window_pos to
// ignore the z-order paramter.
if !top {
flags |= SetWindowPosition::NO_Z_ORDER;
}
let shadow_rect = Self::shadow_rect(hwnd).unwrap_or_default();
let rect = Rect {
left: layout.left + shadow_rect.left,
top: layout.top + shadow_rect.top,
@@ -357,51 +379,39 @@ impl WindowsApi {
bottom: layout.bottom + shadow_rect.bottom,
};
let position = if top { HWND_TOP } else { HWND_BOTTOM };
Self::set_window_pos(hwnd, &rect, position, flags.bits())
// Note: earlier code had set HWND_TOPMOST here, but we should not do
// that. HWND_TOPMOST is a sticky z-order change, rather than a regular
// z-order reordering. Programs will use TOPMOST themselves to do things
// such as making sure that their tool windows or dialog pop-ups are
// above their main window. If any such windows are unmanaged, they must
// still remian topmost, so we set HWND_TOP here, which will cause the
// managed window to come to the front, but if the managed window has a
// child that is TOPMOST it will still be rendered above, in the proper
// order expected by the application. It's also important to understand
// that TOPMOST is somewhat viral, in that when you set a window to
// TOPMOST all of its owned windows are also made TOPMOST.
// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos#remarks
Self::set_window_pos(hwnd, &rect, HWND_TOP, flags.bits())
}
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
unsafe { BringWindowToTop(hwnd) }.process()
}
// Raise the window to the top of the Z order, but do not activate or focus
// it. Use raise_and_focus_window to activate and focus a window.
pub fn raise_window(hwnd: HWND) -> Result<()> {
let flags = SetWindowPosition::NO_MOVE;
let flags = SetWindowPosition::NO_MOVE | SetWindowPosition::NO_ACTIVATE;
let position = HWND_TOP;
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.
let position = HWND_NOTOPMOST;
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())
}
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())
}
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
unsafe {
@@ -418,7 +428,7 @@ impl WindowsApi {
.process()
}
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
pub fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
// BOOL is returned but does not signify whether or not the operation was succesful
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
unsafe { ShowWindow(hwnd, command) };
@@ -459,8 +469,31 @@ impl WindowsApi {
unsafe { GetForegroundWindow() }.process()
}
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
unsafe { SetForegroundWindow(hwnd) }.ok().process()
pub fn raise_and_focus_window(hwnd: HWND) -> Result<()> {
let event = [INPUT {
r#type: INPUT_MOUSE,
..Default::default()
}];
unsafe {
// Send an input event to our own process first so that we pass the
// foreground lock check
SendInput(&event, size_of::<INPUT>() as i32);
// Error ignored, as the operation is not always necessary.
let _ = SetWindowPos(
hwnd,
HWND_TOP,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
)
.process();
SetForegroundWindow(hwnd)
}
.ok()
.process()
}
#[allow(dead_code)]
@@ -477,6 +510,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()?;
@@ -527,6 +570,24 @@ impl WindowsApi {
})
}
pub fn round_rect(hdc: HDC, rect: &Rect, border_radius: i32) {
unsafe {
RoundRect(
hdc,
rect.left,
rect.top,
rect.right,
rect.bottom,
border_radius,
border_radius,
);
}
}
pub fn rectangle(hdc: HDC, rect: &Rect) {
unsafe {
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
}
}
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
unsafe { SetCursorPos(x, y) }.process()
}
@@ -562,10 +623,6 @@ impl WindowsApi {
(process_id, thread_id)
}
pub fn current_thread_id() -> u32 {
unsafe { GetCurrentThreadId() }
}
pub fn current_process_id() -> u32 {
unsafe { GetCurrentProcessId() }
}
@@ -583,16 +640,6 @@ impl WindowsApi {
}
}
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
.ok()
.process()
}
pub fn set_focus(hwnd: HWND) -> Result<()> {
unsafe { SetFocus(hwnd) }.process().map(|_| ())
}
#[allow(dead_code)]
fn set_window_long_ptr_w(
hwnd: HWND,
@@ -744,20 +791,32 @@ impl WindowsApi {
}
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
let ex_info = Self::monitor_info_w(HMONITOR(hmonitor))?;
let name = U16CStr::from_slice_truncate(&ex_info.szDevice)
.expect("monitor name was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\.\")
.to_string();
for display in win32_display_data::connected_displays().flatten() {
if display.hmonitor == hmonitor {
let path = display.device_path;
let mut split: Vec<_> = path.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
let device = split[0].to_string();
let device_id = split.join("-");
Ok(monitor::new(
hmonitor,
ex_info.monitorInfo.rcMonitor.into(),
ex_info.monitorInfo.rcWork.into(),
name,
))
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
let monitor = monitor::new(
hmonitor,
display.size.into(),
display.work_area_size.into(),
name,
device,
device_id,
);
return Ok(monitor);
}
}
bail!("could not find device_id for hmonitor: {hmonitor}");
}
pub fn set_process_dpi_awareness_context() -> Result<()> {
@@ -914,7 +973,7 @@ impl WindowsApi {
None,
);
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd
}
@@ -951,12 +1010,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)]
@@ -964,6 +1017,13 @@ impl WindowsApi {
actual != 0
}
pub fn lbutton_is_pressed() -> bool {
let state = unsafe { GetKeyState(i32::from(VK_LBUTTON.0)) };
#[allow(clippy::cast_sign_loss)]
let actual = (state as u16) & 0x8000;
actual != 0
}
pub fn left_click() -> u32 {
let inputs = [
INPUT {
@@ -999,4 +1059,8 @@ impl WindowsApi {
SendInput(&inputs, std::mem::size_of::<INPUT>() as i32)
}
}
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
unsafe { WTSRegisterSessionNotification(HWND(hwnd), 1) }.process()
}
}

View File

@@ -1,148 +1,17 @@
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;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::window::RuleDebug;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
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;
pub extern "system" fn valid_display_monitors(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<(String, isize)>) };
if let Ok(m) = WindowsApi::monitor(hmonitor.0) {
monitors.push((m.name().to_string(), hmonitor.0));
}
true.into()
}
pub extern "system" fn enum_display_monitor(
hmonitor: HMONITOR,
_: HDC,
_: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
// Don't duplicate a monitor that is already being managed
for monitor in monitors.elements() {
if monitor.id() == hmonitor.0 {
return true.into();
}
}
let current_index = monitors.elements().len();
if let Ok(mut m) = WindowsApi::monitor(hmonitor.0) {
#[allow(clippy::cast_possible_truncation)]
if let Ok(d) = WindowsApi::enum_display_devices(current_index as u32, None) {
let name = U16CStr::from_slice_truncate(d.DeviceName.as_ref())
.expect("display device name was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\.\")
.to_string();
if name.eq(m.name()) {
if let Ok(device) = WindowsApi::enum_display_devices(0, Some(d.DeviceName.as_ptr()))
{
let id = U16CStr::from_slice_truncate(device.DeviceID.as_ref())
.expect("display device id was not a valid u16 c string")
.to_ustring()
.to_string_lossy()
.trim_start_matches(r"\\?\")
.to_string();
let mut split: Vec<_> = id.split('#').collect();
split.remove(0);
split.remove(split.len() - 1);
m.set_device(Option::from(split[0].to_string()));
m.set_device_id(Option::from(split.join("-")));
}
}
}
let monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
let mut index_preference = None;
for (index, monitor_size) in &*monitor_index_preferences {
if m.size() == monitor_size {
index_preference = Option::from(index);
}
}
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, device) in &*display_index_preferences {
if let Some(known_device) = m.device_id() {
if device == known_device {
index_preference = Option::from(index);
}
}
}
if monitors.elements().is_empty() {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
let current_len = monitors.elements().len();
if *preference > current_len {
monitors.elements_mut().reserve(1);
}
monitors.elements_mut().insert(*preference, m);
} else {
monitors.elements_mut().push_back(m);
}
}
true.into()
}
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
@@ -155,7 +24,7 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
if is_visible && is_window && !is_minimized {
let window = Window { hwnd: hwnd.0 };
if let Ok(should_manage) = window.should_manage(None) {
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
if should_manage {
if is_maximized {
WindowsApi::restore_window(hwnd);
@@ -171,6 +40,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,
@@ -191,107 +80,23 @@ pub extern "system" fn win_event_hook(
Ok(event) => event,
Err(_) => return,
};
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => return,
None => {
tracing::trace!(
"Unhandled WinEvent: {winevent} (hwnd: {}, exe: {}, title: {}, class: {})",
window.hwnd,
window.exe().unwrap_or_default(),
window.title().unwrap_or_default(),
window.class().unwrap_or_default()
);
return;
}
Some(event) => event,
};
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
if should_manage {
winevent_listener::event_tx()
.send(event_type)
.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.
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);
}
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,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match message {
WM_DISPLAYCHANGE => {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
LRESULT(0)
}
// Added based on this https://stackoverflow.com/a/33762334
WM_SETTINGCHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == SPI_SETWORKAREA.0
|| wparam.0 as u32 == SPI_ICONVERTICALSPACING.0
{
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}
// Added based on this https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
winevent_listener::event_tx()
.send(event_type)
.expect("could not send message on winevent_listener::event_tx");
}

Some files were not shown because too many files have changed in this diff Show More