Compare commits

..

61 Commits

Author SHA1 Message Date
LGUG2Z
557ef98ee5 feat(wm): add tiling and floating ws layers
This commit introduces an implementation of workspace layers to
komorebi.

Workspace layers change the kinds of windows that certain commands
operate on. This implementation features two variants,
WorkspaceLayer::Tiling and WorkspaceLayer::Floating.

The default behaviour until now has been WorkspaceLayer::Tiling.

When the user sets WorkspaceLayer::Floating, either through the
'toggle-workspace-layer' command or the new bar widget, the 'move',
'focus', 'cycle-focus' and 'resize-axis' commands will operate on
floating windows, if the currently focused window is a floating window.

As I don't have 'cycle-focus' bound to anything, 'focus up' and 'focus
down' double as incrementing and decrementing cycle focus commands,
iterating focus through the floating windows assigned to a workspace.

Floating windows in komorebi belong to specific workspaces, therefore
commands such as 'move' and 'resize-axis' will restrict movement and
resizing to the bounds of their workspace's work area (or more
accurately, the work area of the monitor that the workspace belongs to,
as floating windows are never constrained by workspace-specific work
area restrictions).
2025-02-22 12:54:33 -08:00
alex-ds13
3d327c407c perf(reaper): switch to channel notifications
This commit changes the way the reaper works.

First this commit changed the `known_hwnds` held by the `WindowManager`
to be a HashMap of window handles (isize) to a pair of monitor_idx,
workspace_idx (usize, usize).

This commit then changes the reaper to have a cache of hwnds which is
updated by the `WindowManager` when they change. The reaper has a thread
that is continuously checking this cache to see if there is any window
handle that no longer exists. When it finds them, the thread sends a
notification to a channel which is then received by the reaper on
another thread that actually does the work on the `WindowManager` by
removing said windows.

This means that the reaper no longer tries to access and lock the
`WindowManager` every second like it used to, but instead it only does
it when it actually needs, when a window actually needs to be reaped.
This means that we can make the thread that checks for orphan windows
run much more frequently since it won't influence the rest of komorebi.

Since now the `known_hwnds` have the monitor/workspace index pair of the
window, we can simply get that info from the map and immediately access
that monitor/workspace or use that index info.
2025-02-22 12:29:27 -08:00
alex-ds13
e5fb5390a8 feat(wm): strip unncessary info from state 2025-02-22 12:28:30 -08:00
alex-ds13
6a8e362c21 refactor(wm): make workspace fields public 2025-02-22 12:28:23 -08:00
alex-ds13
1edeb44203 fix(wm): include workspace rules on cached monitor
The `WorkspaceConfig` stored on `Workspace` was changed to not be
serialized, however it needs to be serialized and deserialized when
caching a monitor (after a disconnect), so that when it reconnects it is
able to read all the workspace rules, which include:

- initial_workspace_rules
- workspace_rules
- window_container_behaviour_rules
- layout_rules
- custom_layout_rules

This commit changes the serde skip to only skip if is is `None`.

This means that the `komorebic state` command will have all this
information as well and it will send it when notifying subscribers too,
which isn't good at all, so we need to find another way of excluding it
from the state.
2025-02-22 12:27:55 -08:00
LGUG2Z
8bc04f0610 chore(deps): bump windows-rs from 0.58 to 0.60 2025-02-21 20:16:50 -08:00
LGUG2Z
30c22f51c9 feat(cli): add toggle-window-based-work-area-offset cmd
This commit adds a command to toggle the application of a monitor's
window-based work area offset for the focused workspace.

resolve #1285
2025-02-20 20:38:12 -08:00
alex-ds13
c095f8ae9f fix(bar): improve handle monitor lifecycle handling
This commit introduces a few changes to the bar so that it can handle
the monitor disconnect/reconnect properly and so that it can map the bar
config to the correct monitor.

Previously if you had 3 monitor configs setup on `komorebi.json` with
the `display_index_preferences` set like this:

```
"display_index_preferences": [
    "0": "MONITOR_A",
    "1": "MONITOR_B",
    "2": "MONITOR_C",
]
```

But if you only had connected monitors A and C you would have to
manually change the bar configurations monitor index because now monitor
A would have index 0, but monitor C would have index 1 instead of 2.

Now with this commit this is no longer needed. Now the monitor index
setup on the bar configuration **MUST BE** the index you've used on
`display_index_preferences` for that monitor.

So in the case above you would setup the bar configurations using the
indices 0, 1 and 2 for monitors A, B and C respectively.

As for the changes introduced on this commit they are the following:

- `Komobar.monitor_index` is now an `Option`. When it is `None` it
  either means that the bar is starting and has not yet received the
  first `State` from komorebi or that the bar is disabled.

- `Komobar.config` is no longer an `Arc`. There was no need for that and
  it was creating more issues and difficulties. It was mainly used to
  pass the config as an `Arc` to `apply_config` function, but then this
  function would actually clone the config itself (not just the `Arc`).

  Also this function was passing a `self.config.clone()` most of the
  times except once when it received a new config from a hotreload. Now,
  on hotreload it first sets `self.config` to the new config and then
  calls `apply_config` which now uses its own `self.config` everywhere.

- We only change the `work_area_offset` when the bar is not disabled.

- We update the global `MONITOR` size/coordinates when we receive a
  `DisplayConnectionChange` from komorebi.

- We also check if the monitor size/coordinates have changed from the
  currently stored ones on every komorebi notification since sometimes
  the `DisplayConnectionChange` would be emitted while the state still
  had the previous size/coordinates.

  This makes sure we always capture a change of size/coordinates, store
  the new values, and update the bar.

- The previously mentioned update of the `MONITOR` coordinates also
  updates the `config.position.start` since that value is being
  overriden on `main.rs` in case the user hasn't set it so we need to
  override it again with the new monitor coordinates.

  This might mean that users of the old config system might have their
  start position changed, but if we didn't do this the bar wouldn't even
  show on the screen for them when a monitor disconnected/reconnected.

  This is another case for users to start moving into the new config
  system, since with that system the bar will still show up with the
  correct margins!
2025-02-20 19:57:36 -08:00
alex-ds13
c455ad1386 feat(wm): register more monitor reconcilator events
This commits adds a few more events that can trigger a
`DisplayConnectionChange` event.

Some of these events are redundant and after a display is
disconnected/reconnected it emits multiple `DisplayConnectionChange`
events.

However, when trying to remove them to have just one it stopped behaving
as it should, as if it was missing the update, while having them
duplicated it works properly.

Therefore it appears to be better to keep them for now, since the
duplicated events will exit early as soon as they see that the monitor
counts match (on the first event the counts don't match so it
adds/removes the monitor and the following events see that the counts
match).
2025-02-20 19:55:24 -08:00
alex-ds13
ce99290027 chore(deps): update win32-display-data rev 2025-02-20 19:55:19 -08:00
alex-ds13
60bc83d407 fix(wm): increase monitor_reconciliator channel bound
When there is a monitor disconnect/reconnect, usually it produces
multiple monitor events along with it, like the monitor resolution
change and the work area change. With the bound set to 1 it would
sometimes result in missed events.

This commit increases the bound to 20 to prevent this from happening.
2025-02-20 19:54:55 -08:00
alex-ds13
9c8a639282 fix(wm): check for monitor changes on system resume 2025-02-20 19:54:34 -08:00
alex-ds13
b7ebd3fe63 fix(bar): check monitor connection on all notifications 2025-02-20 19:54:27 -08:00
alex-ds13
ec8519d75a fix(wm): don't panic if state isn't up to date
This commit makes sure that if the state on file isn't up to date with
the expected `State` struct (maybe after an update) it doesn't panic
komorebi entirely, instead it ignores the state and continues with a
clean state.
2025-02-20 19:54:17 -08:00
alex-ds13
c62405bfaa fix(bar): restore + reposition on monitor reconnect 2025-02-20 19:54:05 -08:00
alex-ds13
0126465de4 fix(wm): cache monitor state instead of config
This commit changes the MONITOR_CACHE to hold an actual monitor with its
state instead of the config.

This allows us to keep the state of a disconnected monitor, so that when
the monitor reconnects we still have access to all its workspaces and
windows as they were before.

While the monitor is disconnected, all the windows from that monitor are
not considered as handled by komorebi and so they're free to be handled
at any point; if for example the user uses `alt+tab` or presses the
taskbar icon of one of these windows, that window will produce a `Show`
event and will become handled by the currently focused workspace of the
focused monitor.

When the disconnected monitor reconnects it checks all windows and once
it notices that this specific window is now being shown on another
monitor/workspace, it ignores it.
2025-02-20 19:52:38 -08:00
alex-ds13
1cd28652aa feat(wm): keep track of known_hwnds on wm 2025-02-20 19:52:15 -08:00
alex-ds13
a1ab1c5724 fix(wm): update usr idx map when there are no index preferences 2025-02-20 19:52:00 -08:00
alex-ds13
be932078e0 refactor(wm): make monitor fields public 2025-02-20 19:51:49 -08:00
alex-ds13
302e96c172 fix(bar): handle monitor disconnect/reconnect
This commit allows a bar to be disabled when it's monitor is
disconnected and re-enabled when said monitor reconnects.

This commit also uses the new `monitor_usr_idx_map` to properly map the
monitor index given by the users on config to the actual monitor index
on the `WindowManager` - in case some middle monitor gets disconnected
the index of the monitors to the "right" of that one will be lowered by
1.

This should allow for the following in cases with monitor A, B and C: if
monitor B disconnects, its bar will be disabled and monitor C will
properly change its monitor index internally to 1 so it still gets the
infos from monitor C (which now will have index 1 on the
`WindowManager`).
2025-02-20 19:49:47 -08:00
alex-ds13
c05eab9044 feat(wm): add monitor_usr_idx_map to wm
This commit creates a new field on the `WindowManager` called
`monitor_usr_idx_map` which contains a map of user intended index for
monitors to their actual monitors' index.

It will be rebuilt on `load_monitor_information` by taking into account
the `display_index_preferences`.
2025-02-20 19:49:21 -08:00
alex-ds13
ff986fba67 fix(wm): remove ws rules from disconnected monitors
This commit makes sure that any workspace_rules that tried to move a
window to a monitor that has been disconnected are removed.

If the monitor is later reconnected the workspace_rules should be added
from the cached config.
2025-02-20 19:48:56 -08:00
alex-ds13
e408410c58 fix(wm): handle serial id on load_monitor_information 2025-02-20 19:48:38 -08:00
alex-ds13
3ade81444a feat(wm): support both serial numbers and device ids
This commit makes use of both `serial_number_id` and `device_id` with
the first taking priority on all monitor reconciliator code, monitor
cache and on postload and reload.

This allows using the serial numbers on the `display_index_preferences`
config, while keeping compatibility with the use of `device_id`.

Using `device_id` should be discouraged since that value can
change on restart while serial number doesn't appear to do so.
2025-02-20 19:47:25 -08:00
alex-ds13
c9e98c3cdb fix(wm): serde skip annotation for workspace_config 2025-02-20 19:46:36 -08:00
alex-ds13
b42fcbe509 fix(wm): restore orphaned containers
When a monitor was disconnected the containers from the removed monitor
were being moved to the primary monitor.

However they weren't restored so containers that were on an unfocused
workspace of the removed monitor would have been cloak and were getting
added to the main monitor still cloaked creating ghost tiles. This
commit fixes that.
2025-02-20 19:46:21 -08:00
alex-ds13
d8636d651d fix(wm): don't store empty layout_rules on monitor cache 2025-02-20 19:46:14 -08:00
alex-ds13
9ad32e40cf fix(wm): cache monitor configs for unloaded monitors
If we have display_index_preferences that set a specific config index
for a specific display device, but that device isn't loaded yet, now we
store that config with the corresponding `device_id` on the monitor
cache.

Now when the display is connected it can load the correct config from
the cache.
2025-02-20 19:45:31 -08:00
alex-ds13
c91cb9f061 fix(wm): improve display_index_preferences selection
This commit reworks the way the `postload` and the `reload` functions
apply the monitor configs to the monitors.

Previously it was looping through the monitor configs and applying them
to the monitor with the index corresponding to the config's index.

However this isn't correct, since the user might set the preferred
indices for 3 monitors (like monitor A, B and C), with the preferred
index set to 0 for A, 1 for B and 2 for C, but if only monitors A and C
are connected then komorebi would apply config 0 to A and config 1 to C,
which is wrong it should be 2 for C.

This commit changes the way the configs are applied on those functions.
Now it loops through the existing monitors (already in order), then
checks if the monitor has a preferred config index, if it does it uses
that one, if it doesn't then it uses the first monitor config that isn't
a preferred index for some other monitor and that hasn't been used yet.

For the situation above it means that it would still apply config 2 to
monitor C. And in case there aren't any display_index_preferences set it
will still apply the configs in order.
2025-02-20 19:44:11 -08:00
alex-ds13
52340a1487 refactor(wm): store config on workspace
Store the `WorkspaceConfig` on the `Workspace` itself so that when we
want to cache the workspace as `WorkspaceConfig` on the monitor cache it
properly saves things like the workspace rules and the custom layout and
custom layout rules.
2025-02-20 19:43:53 -08:00
alex-ds13
4f7a8f10c0 fix(wm): properly store tile state when caching ws
Previously, when caching a workspace config for a monitor it would
simply store the `DefaultLayout` on `layout` even if the original
workspace config had the `layout` as `None`, which makes komorebi create
a workspace with the `layout` as default `BSP` and the `tile` set to
`false`.

This resulted in floating workspaces would becoming tiling `BSP`
workspaces after a monitor disconnect and reconnect.

This commit fixes this by turning the `layout` to `None` when `tile` is
`false`.
2025-02-20 19:42:33 -08:00
LGUG2Z
c903cdbb75 chore(dev): begin v0.1.35-dev 2025-02-20 19:31:27 -08:00
LGUG2Z
80edcadbf7 chore(release): v0.1.34 2025-02-20 18:06:19 -08:00
dependabot[bot]
36dedbe3fe chore(deps): bump os_info from 3.9.2 to 3.10.0
Bumps [os_info](https://github.com/stanislav-tkach/os_info) from 3.9.2 to 3.10.0.
- [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.9.2...v3.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 08:03:40 -08:00
Csaba
5f31e89e8d feat(bar): add thai font fallback 2025-02-17 05:53:25 -08:00
Csaba
d168013375 fix(bar): removed unneeded separator character on network widget 2025-02-14 15:36:01 -08:00
David
db6e12b0c2 perf(wm): reduce from sysinfo scan scope
System::new_all() pulls all information (processes, cpu, mem, etc) but
we only need process information.

In addition currently it is being polled twice. System::new() creates an
uninitialized struct, then we poll specifically for process info.
2025-02-14 15:33:19 -08:00
pro470
e629baec0a feat(client): expose custom layout column enum
This now allows integrators to deal with custom layout data.
2025-02-14 15:32:32 -08:00
Pierce Thompson
475519d603 fix(wm): set default hiding behaviour to cloak
Documentation for the `window_hiding_behaviour` option states
that it defaults to `Cloak`, however, it was actually
defaulting to `Minimize`.
2025-02-14 15:31:00 -08:00
David
2d2b6e5c15 feat(bar): add keyboard language widget
This commit is a squashed commit containing the below commits from
PR #1266, which introduces a new "Keyboard" widget, which is used to
display information about the user's currently selected keyboard input
language. This new widget has a data refresh interval of 1 second if not
specified by the user.

721d2ef408
58373cd26c
ce27a76b36
fb9054a18b
55cc2fd889
461a73833e
781b8d0bd0
fa6bf6ff76
2025-02-07 20:57:46 -08:00
Mike Shaver
9f19d449b2 fix(docs): correct sp in example-configurations.md 2025-02-07 15:54:31 -08:00
alex-ds13
86bbcac5ae feat(client): add more re-exports for integrations
This commit is a squashed commit of all the individual commits that made
up PR #1267 - adding various derives and re-exports aimed at improving
the komorebi integration surface for third party applications.
2025-02-07 15:53:38 -08:00
DhanushAdithiya
bbd232f649 fix(docs): update base16 gallery link 2025-02-05 13:13:49 -08:00
LGUG2Z
2ca9c9048b build(just): add build and build-target(s) to justfile 2025-02-04 16:08:57 -08:00
LGUG2Z
95d758e371 fix(wm): avoid focus loops on ws w/ floating hwnds
This commit adds a check which will only allow the focused workspace to
have a full update if the number of managed containers is non-zero.

Previously, this would be triggered in a loop when focusing a workspace
with only focused windows.

Going back in time to the first versions of komorebi and yatta which
didn't have so many different container and window kinds, this was
intended to be called whenever the focus was changed to update the
state.

With the complexity komorebi handles in 2025, there are also many calls
to Win32 APIs when we call self.update_focused_workspace, so we need to
be a bit more careful about when and where we call it.

re #816
2025-02-03 20:18:17 -08:00
LGUG2Z
83114ed3e7 chore(deps): cargo update 2025-02-03 19:21:46 -08:00
LGUG2Z
f3075efcae fix(wm): always preserve resize on monocle toggle
This commit ensures that the Vec of resize adjustment Rects will never
be truncated when monocle mode is enabled for a container.

fix #1257
2025-02-01 20:02:48 -08:00
LGUG2Z
80b611890a fix(reaper): reap invisible "visible" windows
Another day, another stupid hack because Microsoft Office developers are
morons.
2025-02-01 00:15:06 -08:00
LGUG2Z
58d660eb16 feat(borders): add floating colour for windows impl
This commits adds support for focused floating window colours on borders
when using the "Windows" border implementation.
2025-01-28 15:58:19 -08:00
LGUG2Z
afdbce3db1 fix(borders): respond to sys foreground winevent w/ border manager event
This commit ensures that we emit a dedicated border manager event when
WinEvent::SystemForeground is received.

The OS can actually be slower than komorebi when it comes to processing
changed focus state, and in the border manager we rely on
GetForegroundWindow when calculating which the border focus state and
color should be.

This has previously resulted in a situation where there may be no border
with the "focused" color.

This should no longer be a problem because even in the situations where
the OS is slower than komorebi and is still returning an old HWND from
GetForegroundWindow, the new event that we emit to border manager in
response to WinEvent::SystemForeground will ensure that the border focus
colors get updated.
2025-01-27 21:16:40 -08:00
LGUG2Z
be8af2b314 feat(cli): add focus-monitor-at-cursor cmd
This commit adds a new komorebic command, focus-monitor-at-cursor, which
can optionally be chained with the focus-workspace command in
keybindings to reproduce the previous default behaviour of auto-focusing
whichever monitor the cursor was on before attempting to change the
focused workspace.
2025-01-27 16:59:50 -08:00
dependabot[bot]
241f8a1375 chore(deps): bump getset from 0.1.3 to 0.1.4
Bumps [getset](https://github.com/jbaublitz/getset) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/jbaublitz/getset/releases)
- [Commits](https://github.com/jbaublitz/getset/compare/0.1.3...0.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 16:53:31 -08:00
dependabot[bot]
bd0913a5f5 chore(deps): bump clap from 4.5.26 to 4.5.27
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.26 to 4.5.27.
- [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.26...clap_complete-v4.5.27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 16:40:26 -08:00
alex-ds13
7f3b932693 fix(wm): sort layout and behaviour rules
This commit makes sure the `layout-rules` and
`window_container_behaviour_rules` are sorted when setting them from the
config. So that the behaviour on workspace update is correct.
2025-01-27 14:15:02 -08:00
LGUG2Z
59cd36a2b1 fix(config): unset values on ws configs appropriately
This commit ensures that if a user removes an optional block from the
static config file, when reloading a workspace config, the removed
option will also be unset in the window manager workspace configuration
state.
2025-01-27 08:42:42 -08:00
LGUG2Z
b8e8ac2cc9 fix(wm): handle empty ws monitor switch w/ mff off
This commit improves the handling of the situation where a user, with
mouse-follows-focus diabled, focuses a secondary monitor with an empty
workspace, either via a komorebic command or by moving the cursor and
clicking on that empty workspace, and then attempts to switch
workspaces.

Previously, if the focus was made by a komorebic command, the mouse
cursor would not move from the previous monitor, and then when trying to
switch the workspace, the previous monitor would be focused against
first. The only way to change focus would be to move the mouse to the
secondary monitor.

With these changes, the following situations all work as expected:

* MFF On + MFF Off: komorebic cmd to focus an empty workspace on a
  secondary monitor allows subsequent focus-workspace cmds to execute on
  the newly focused secondary monitor

* MFF On + MFF Off: Moving the cursor to an empty workspace on a
  secondary monitor allows subsequent focus-workspace cmds to execute on
  the newly focused secondary monitor

There is one slight change in behaviour:

* MFF On + MFF Off: When the cursor is on a populated workspace on a
  secondary monitor which is not focused, focus-workspace cmds will not
  execute on that secondary monitor, but on the currently focused
  monitor

resolve #831
resolve #1128
2025-01-26 19:31:23 -08:00
LGUG2Z
c364b90b3b feat(config): add window container behaviour rules
This commit adds a new static config option,
window_container_behaviour_rules, which similarly to layout_rules, takes
a map of window container count threshold => window container behaviour.

When the number of window containers on the screen meets a given
threshold, the new window container behaviour is applied to the
workspace.

This can be used to automatically change from creating new window
containers for new windows to appending new windows to existing window
containers when the number of window containers on the screen reaches
more than what can be comfortably laid out and viewed on a user's
screen.

resolve #953
2025-01-25 22:02:50 -08:00
LGUG2Z
e2f7fe50c9 feat(wm): use monitor hardware ids where available
This commit pulls in changes to win32-display-data which provide the
monitor hardware serial number id taken from WmiMonitorID where
available.

No work has yet been done to integrate this with options such as
display_index_preferences.
2025-01-25 22:02:50 -08:00
LGUG2Z
81c143d7c2 feat(config): add object name change title ignore list
This commit adds a title regex-based ignore list for applications
identified in object_name_change_applications. When a title change on an
EVENT_OBJECT_NAMECHANGE matches one of these regexes, the event will
never be processed as a Show.

This is an edge case workaround specifically targeting the issue of web
apps in Gecko-based browsers which update their page titles at a fixed
regular interval, which was highlighted in #1235.

resolve #1235
2025-01-25 22:02:50 -08:00
LGUG2Z
fcd1c9dcbe fix(wm): populate ws rules on config reload
This commit fixes a bug where workspace rules would not be populated
properly on file reloads, leading to issues with the
ReplaceConfiguration message handler.
2025-01-25 22:02:50 -08:00
LGUG2Z
f73f0a0012 fix(bar): consider all window types when hiding empty ws
This commit ensures that floating windows, monocle containers and
maximized windows will be considered when the hide_empty_workspaces
option is enabled for the komorebi widget.

re #1131
2025-01-25 22:02:46 -08:00
57 changed files with 3041 additions and 900 deletions

567
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.30"
egui_extras = "0.30"
dirs = "5"
dirs = "6"
dunce = "1"
hotwatch = "0.5"
schemars = "0.8"
@@ -33,27 +33,31 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.33"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "75286e77c068a89d12adcd6404c9c4874a60acf5" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.37"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "376523b9e1321e033b0b0ed0e6fa75a072b46ad9" }
windows-numerics = { version = "0.1" }
windows-implement = { version = "0.59" }
windows-interface = { version = "0.59" }
windows-core = { version = "0.60" }
shadow-rs = "0.38"
which = "7"
[workspace.dependencies.windows]
version = "0.58"
version = "0.60"
features = [
"implement",
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",

View File

@@ -389,7 +389,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.33"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.34"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -1,7 +1,7 @@
# flip-layout
```
Flip the layout on the focused workspace (BSP only)
Flip the layout on the focused workspace
Usage: komorebic.exe flip-layout <AXIS>

View File

@@ -0,0 +1,12 @@
# focus-monitor-at-cursor
```
Focus the monitor at the current cursor location
Usage: komorebic.exe focus-monitor-at-cursor
Options:
-h, --help
Print help
```

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
Arguments:
<STATE_QUERY>
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index]
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name]
Options:
-h, --help

View File

@@ -0,0 +1,12 @@
# toggle-window-based-work-area-offset
```
Toggle application of the window-based work area offset for the focused workspace
Usage: komorebic.exe toggle-window-based-work-area-offset
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# toggle-workspace-layer
```
Toggle between the Tiling and Floating layers on the focused workspace
Usage: komorebic.exe toggle-workspace-layer
Options:
-h, --help
Print help
```

View File

@@ -75,7 +75,7 @@ solo developer.
If you choose to use the active window border, you can set different colours to
give you visual queues when you are focused on a single window, a stack of
windows, or a window that is in monocole mode.
windows, or a window that is in monocle mode.
The example colours given are blue single, green for stack and pink for
monocle.
@@ -254,5 +254,5 @@ stackbars as well as the status bar.
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
All [Catppuccin palette variants](https://catppuccin.com/)
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
and [most Base16 palette variants](https://tinted-theming.github.io/tinted-gallery/)
are available as themes.

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.33/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.bar.json",
"monitor": 0,
"font_family": "JetBrains Mono",
"theme": {

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.33/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

View File

@@ -24,6 +24,15 @@ install-target target:
install:
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
build-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
build-target target:
cargo +stable build --release --package {{ target }} --locked
build:
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
run target:
cargo +stable run --bin {{ target }} --locked

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.34"
version = "0.1.35"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -34,4 +34,5 @@ sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }

View File

@@ -60,8 +60,9 @@ use std::sync::Arc;
pub struct Komobar {
pub hwnd: Option<isize>,
pub monitor_index: usize,
pub config: Arc<KomobarConfig>,
pub monitor_index: Option<usize>,
pub disabled: bool,
pub config: KomobarConfig,
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
@@ -194,46 +195,45 @@ impl Komobar {
pub fn apply_config(
&mut self,
ctx: &Context,
config: &KomobarConfig,
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
) {
MAX_LABEL_WIDTH.store(
config.max_label_width.unwrap_or(400.0) as i32,
self.config.max_label_width.unwrap_or(400.0) as i32,
Ordering::SeqCst,
);
if let Some(font_family) = &config.font_family {
if let Some(font_family) = &self.config.font_family {
tracing::info!("attempting to add custom font family: {font_family}");
Self::add_custom_font(ctx, font_family);
}
// Update the `size_rect` so that the bar position can be changed on the EGUI update
// function
self.update_size_rect(config);
self.update_size_rect();
self.try_apply_theme(config, ctx);
self.try_apply_theme(ctx);
if let Some(font_size) = &config.font_size {
if let Some(font_size) = &self.config.font_size {
tracing::info!("attempting to set custom font size: {font_size}");
Self::set_font_size(ctx, *font_size);
}
self.render_config.replace(config.new_renderconfig(
self.render_config.replace((&self.config).new_renderconfig(
ctx,
*self.bg_color.borrow(),
config.icon_scale,
self.config.icon_scale,
));
let mut komorebi_notification_state = previous_notification_state;
let mut komorebi_widgets = Vec::new();
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Left));
}
}
if let Some(center_widgets) = &config.center_widgets {
if let Some(center_widgets) = &self.config.center_widgets {
for (idx, widget_config) in center_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Center));
@@ -241,20 +241,21 @@ impl Komobar {
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
for (idx, widget_config) in self.config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Right));
}
}
let mut left_widgets = config
let mut left_widgets = self
.config
.left_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut center_widgets = match &config.center_widgets {
let mut center_widgets = match &self.config.center_widgets {
Some(center_widgets) => center_widgets
.iter()
.filter(|config| config.enabled())
@@ -263,7 +264,8 @@ impl Komobar {
None => vec![],
};
let mut right_widgets = config
let mut right_widgets = self
.config
.right_widgets
.iter()
.filter(|config| config.enabled())
@@ -305,79 +307,87 @@ impl Komobar {
self.center_widgets = center_widgets;
self.right_widgets = right_widgets;
let (monitor_index, config_work_area_offset) = match &config.monitor {
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
(monitor_config.index, monitor_config.work_area_offset)
}
MonitorConfigOrIndex::Index(idx) => (*idx, None),
};
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
state
.borrow()
.monitor_usr_idx_map
.get(&usr_monitor_index)
.copied()
});
self.monitor_index = monitor_index;
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset) {
if new_rect != prev_rect {
self.work_area_offset = *new_rect;
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(self.monitor_index, *new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
self.monitor_index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
self.monitor_index
);
if let Some(monitor_index) = self.monitor_index {
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
{
if new_rect != prev_rect {
self.work_area_offset = *new_rect;
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(monitor_index, *new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
monitor_index,
error,
);
} else {
tracing::info!("work area offset applied to monitor: {}", monitor_index);
}
}
}
} else if let Some(height) = config.height.or(Some(BAR_HEIGHT)) {
// We only add the `bottom_margin` to the work_area_offset since the top margin is
// already considered on the `size_rect.top`
let bottom_margin = config
.margin
.as_ref()
.map_or(0, |v| v.to_individual(0.0).bottom as i32);
let new_rect = komorebi_client::Rect {
left: 0,
top: (height as i32)
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
+ bottom_margin,
right: 0,
bottom: (height as i32)
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
+ bottom_margin,
};
} else if let Some(height) = self.config.height.or(Some(BAR_HEIGHT)) {
// We only add the `bottom_margin` to the work_area_offset since the top margin is
// already considered on the `size_rect.top`
let bottom_margin = self
.config
.margin
.as_ref()
.map_or(0, |v| v.to_individual(0.0).bottom as i32);
let new_rect = komorebi_client::Rect {
left: 0,
top: (height as i32)
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
+ bottom_margin,
right: 0,
bottom: (height as i32)
+ (self.size_rect.top - MONITOR_TOP.load(Ordering::SeqCst))
+ bottom_margin,
};
if new_rect != self.work_area_offset {
self.work_area_offset = new_rect;
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(self.monitor_index, new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
self.monitor_index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
self.monitor_index
);
if new_rect != self.work_area_offset {
self.work_area_offset = new_rect;
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(monitor_index, new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
monitor_index,
error,
);
}
}
}
} else if self.komorebi_notification_state.is_some() && !self.disabled {
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
self.disabled = true;
} else {
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
self.disabled = true;
}
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
self.config = config.clone().into();
}
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
fn update_size_rect(&mut self, config: &KomobarConfig) {
let position = config.position.clone().unwrap_or(PositionConfig {
fn update_size_rect(&mut self) {
let position = self.config.position.clone().unwrap_or(PositionConfig {
start: Some(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
@@ -398,11 +408,11 @@ impl Komobar {
y: BAR_HEIGHT,
});
if let Some(height) = config.height {
if let Some(height) = self.config.height {
end.y = height;
}
let margin = get_individual_spacing(0.0, &config.margin);
let margin = get_individual_spacing(0.0, &self.config.margin);
start.y += margin.top;
start.x += margin.left;
@@ -420,16 +430,16 @@ impl Komobar {
};
}
fn try_apply_theme(&mut self, config: &KomobarConfig, ctx: &Context) {
match config.theme {
fn try_apply_theme(&mut self, ctx: &Context) {
match self.config.theme {
Some(theme) => {
apply_theme(
ctx,
theme,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
config.transparency_alpha,
config.grouping,
self.config.transparency_alpha,
self.config.grouping,
self.render_config.clone(),
);
}
@@ -447,8 +457,8 @@ impl Komobar {
},
);
let bar_transparency_alpha = config.transparency_alpha;
let bar_grouping = config.grouping;
let bar_transparency_alpha = self.config.transparency_alpha;
let bar_grouping = self.config.grouping;
let config = home_dir.join("komorebi.json");
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
@@ -510,12 +520,13 @@ impl Komobar {
cc: &eframe::CreationContext<'_>,
rx_gui: Receiver<KomorebiEvent>,
rx_config: Receiver<KomobarConfig>,
config: Arc<KomobarConfig>,
config: KomobarConfig,
) -> Self {
let mut komobar = Self {
hwnd: process_hwnd(),
monitor_index: 0,
config: config.clone(),
monitor_index: None,
disabled: false,
config,
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
left_widgets: vec![],
@@ -531,9 +542,9 @@ impl Komobar {
applied_theme_on_first_frame: false,
};
komobar.apply_config(&cc.egui_ctx, &config, None);
komobar.apply_config(&cc.egui_ctx, None);
// needs a double apply the first time for some reason
komobar.apply_config(&cc.egui_ctx, &config, None);
komobar.apply_config(&cc.egui_ctx, None);
komobar
}
@@ -571,6 +582,7 @@ impl Komobar {
fallbacks.insert("Microsoft YaHei", "C:\\Windows\\Fonts\\msyh.ttc"); // chinese
fallbacks.insert("Malgun Gothic", "C:\\Windows\\Fonts\\malgun.ttf"); // korean
fallbacks.insert("Leelawadee UI", "C:\\Windows\\Fonts\\LeelawUI.ttf"); // thai
for (name, path) in fallbacks {
if let Ok(bytes) = std::fs::read(path) {
@@ -607,6 +619,32 @@ impl Komobar {
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
pub fn position_bar(&self) {
if let Some(hwnd) = self.hwnd {
let window = komorebi_client::Window::from(hwnd);
if let Err(error) = window.set_position(&self.size_rect, false) {
tracing::error!("{}", error.to_string())
}
}
}
fn update_monitor_coordinates(&mut self, monitor_size: &komorebi_client::Rect) {
// Store the new monitor coordinates
MONITOR_TOP.store(monitor_size.top, Ordering::SeqCst);
MONITOR_LEFT.store(monitor_size.left, Ordering::SeqCst);
MONITOR_RIGHT.store(monitor_size.right, Ordering::SeqCst);
// Since the `config.position` is changed on `main.rs` we need to update it here.
// If the user had set up some `start` position, that will be overriden here
// since we have no way to know what was that value since it might have been
// changed on `main.rs`. However if the users use the new configs this won't be
// a problem for them.
if let Some(start) = self.config.position.as_mut().and_then(|p| p.start.as_mut()) {
start.x = monitor_size.left as f32;
start.y = monitor_size.top as f32;
}
}
}
impl eframe::App for Komobar {
// Needed for transparency
@@ -621,19 +659,12 @@ impl eframe::App for Komobar {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(
ctx,
&self.config.clone(),
self.komorebi_notification_state.clone(),
);
self.apply_config(ctx, self.komorebi_notification_state.clone());
}
if let Ok(updated_config) = self.rx_config.try_recv() {
self.apply_config(
ctx,
&updated_config,
self.komorebi_notification_state.clone(),
);
self.config = updated_config;
self.apply_config(ctx, self.komorebi_notification_state.clone());
}
match self.rx_gui.try_recv() {
@@ -646,32 +677,87 @@ impl eframe::App for Komobar {
}
},
Ok(KomorebiEvent::Notification(notification)) => {
let should_apply_config = if matches!(
let state = &notification.state;
let usr_monitor_index = match &self.config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
MonitorConfigOrIndex::Index(idx) => *idx,
};
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
self.monitor_index = monitor_index;
let mut should_apply_config = false;
if self.monitor_index.is_none()
|| self
.monitor_index
.is_some_and(|idx| idx >= state.monitors.elements().len())
{
if !self.disabled {
// Monitor for this bar got disconnected lets disable the bar until it
// reconnects
self.disabled = true;
tracing::warn!(
"This bar's monitor got disconnected. The bar will be disabled until it reconnects..."
);
}
return;
} else {
if self.disabled {
tracing::info!("Found this bar's monitor. The bar will be enabled!");
// Restore the bar in case it has been minimized when the monitor
// disconnected
if let Some(hwnd) = self.hwnd {
let window = komorebi_client::Window::from(hwnd);
if window.is_miminized() {
komorebi_client::WindowsApi::restore_window(hwnd);
}
}
should_apply_config = true;
}
self.disabled = false;
}
if matches!(
notification.event,
NotificationEvent::Monitor(MonitorNotification::DisplayConnectionChange)
) {
let state = &notification.state;
let monitor_index = self.monitor_index.expect("should have a monitor index");
// Store the monitor coordinates in case they've changed
MONITOR_RIGHT.store(
state.monitors.elements()[self.monitor_index].size().right,
Ordering::SeqCst,
);
let monitor_size = state.monitors.elements()[monitor_index].size();
MONITOR_TOP.store(
state.monitors.elements()[self.monitor_index].size().top,
Ordering::SeqCst,
);
self.update_monitor_coordinates(monitor_size);
MONITOR_LEFT.store(
state.monitors.elements()[self.monitor_index].size().left,
Ordering::SeqCst,
);
should_apply_config = true;
}
true
} else {
false
};
if self.disabled {
return;
}
// Check if monitor coordinates/size has changed
if let Some(monitor_index) = self.monitor_index {
let monitor_size = state.monitors.elements()[monitor_index].size();
let top = MONITOR_TOP.load(Ordering::SeqCst);
let left = MONITOR_LEFT.load(Ordering::SeqCst);
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
let rect = komorebi_client::Rect {
top,
left,
bottom: monitor_size.bottom,
right,
};
if *monitor_size != rect {
tracing::info!(
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
monitor_size
);
self.update_monitor_coordinates(monitor_size);
should_apply_config = true;
}
}
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
@@ -690,36 +776,37 @@ impl eframe::App for Komobar {
}
if should_apply_config {
self.apply_config(
ctx,
&self.config.clone(),
self.komorebi_notification_state.clone(),
);
self.apply_config(ctx, self.komorebi_notification_state.clone());
// Reposition the Bar
self.position_bar();
}
}
Ok(KomorebiEvent::Reconnect) => {
if let Err(error) =
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
self.monitor_index,
self.work_area_offset,
))
{
tracing::error!(
"error applying work area offset to monitor '{}': {}",
self.monitor_index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
self.monitor_index
);
if let Some(monitor_index) = self.monitor_index {
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(monitor_index, self.work_area_offset),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
monitor_index,
error,
);
} else {
tracing::info!("work area offset applied to monitor: {}", monitor_index);
}
}
}
}
if self.disabled {
// The check for disabled is performed above, if we get here and the bar is still
// disabled then we should return without drawing anything.
return;
}
if !self.applied_theme_on_first_frame {
self.try_apply_theme(&self.config.clone(), ctx);
self.try_apply_theme(ctx);
self.applied_theme_on_first_frame = true;
}
@@ -734,17 +821,7 @@ impl eframe::App for Komobar {
};
if self.size_rect != current_rect {
if let Some(hwnd) = self.hwnd {
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&self.size_rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
self.position_bar();
}
}

View File

@@ -13,7 +13,7 @@ use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.34`
/// The `komorebi.bar.json` configuration file reference for `v0.1.35`
pub struct KomobarConfig {
/// Bar height (default: 50)
pub height: Option<f32>,
@@ -366,7 +366,7 @@ pub enum KomobarTheme {
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
},

177
komorebi-bar/src/keyboard.rs Executable file
View File

@@ -0,0 +1,177 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
use std::time::Instant;
use windows::Win32::Globalization::LCIDToLocaleName;
use windows::Win32::Globalization::LOCALE_ALLOW_NEUTRAL_NAMES;
use windows::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
const ERROR_TEXT: &str = "Error";
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KeyboardConfig {
/// Enable the Input widget
pub enable: bool,
/// Data refresh interval (default: 1 second)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<KeyboardConfig> for Keyboard {
fn from(value: KeyboardConfig) -> Self {
let data_refresh_interval = value
.data_refresh_interval
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
Self {
enable: value.enable,
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
lang_name: get_lang(),
}
}
}
pub struct Keyboard {
pub enable: bool,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
lang_name: String,
}
/// Retrieves the name of the active keyboard layout for the current foreground window.
///
/// This function determines the active keyboard layout by querying the system for the
/// foreground window's thread ID and its associated keyboard layout. It then attempts
/// to retrieve the locale name corresponding to the keyboard layout.
///
/// # Failure Cases
///
/// This function can fail in two distinct scenarios:
///
/// 1. **Failure to Retrieve the Locale Name**:
/// If the system fails to retrieve the locale name (e.g., due to an invalid or unsupported
/// language identifier), the function will return `Err(())`.
///
/// 2. **Invalid UTF-16 Characters in the Locale Name**:
/// If the retrieved locale name contains invalid UTF-16 sequences, the conversion to a Rust
/// `String` will fail, and the function will return `Err(())`.
///
/// # Returns
///
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
/// invalid UTF-16 characters during conversion.
fn get_active_keyboard_layout() -> Result<String, ()> {
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
// Extract the low word (language identifier) from the keyboard layout handle.
let lang_id = (lcid.0 as u32) & 0xFFFF;
let mut locale_name_buffer = [0; LOCALE_NAME_MAX_LENGTH as usize];
let char_count = unsafe {
LCIDToLocaleName(
lang_id,
Some(&mut locale_name_buffer),
LOCALE_ALLOW_NEUTRAL_NAMES,
)
};
match char_count {
0 => Err(()),
_ => String::from_utf16(&locale_name_buffer[..char_count as usize]).map_err(|_| ()),
}
}
/// Retrieves the name of the active keyboard layout or a fallback error message.
///
/// # Behavior
///
/// - **Success Case**:
/// If [`get_active_keyboard_layout`] succeeds, this function returns the retrieved keyboard
/// layout name as a `String`.
///
/// - **Failure Case**:
/// If [`get_active_keyboard_layout`] fails, this function returns the value of `ERROR_TEXT`
/// as a fallback message. This ensures that the function always returns a valid `String`,
/// even in error scenarios.
///
/// # Returns
///
/// A `String` representing either:
/// - The name of the active keyboard layout, or
/// - The fallback error message (`ERROR_TEXT`) if the layout name cannot be retrieved.
fn get_lang() -> String {
get_active_keyboard_layout()
.map(|l| l.trim_end_matches('\0').to_string())
.unwrap_or_else(|_| ERROR_TEXT.to_string())
}
impl Keyboard {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.last_updated = now;
self.lang_name = get_lang();
}
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("KB: {}", self.lang_name),
LabelPrefix::None | LabelPrefix::Icon => self.lang_name.clone(),
}
}
}
impl BarWidget for Keyboard {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::KEYBOARD.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
});
}
}
}
}

View File

@@ -33,11 +33,13 @@ use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_client::Window;
use komorebi_client::Workspace;
use komorebi_client::WorkspaceLayer;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
@@ -48,6 +50,8 @@ pub struct KomorebiConfig {
pub workspaces: Option<KomorebiWorkspacesConfig>,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Workspace Layer widget
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
/// Configure the Focused Window widget
pub focused_window: Option<KomorebiFocusedWindowConfig>,
/// Configure the Configuration Switcher widget
@@ -74,6 +78,12 @@ pub struct KomorebiLayoutConfig {
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiWorkspaceLayerConfig {
/// Enable the Komorebi Workspace Layer widget
pub enable: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
@@ -121,10 +131,12 @@ impl From<&KomorebiConfig> for Komorebi {
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
stack_accent: None,
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
monitor_usr_idx_map: HashMap::new(),
})),
workspaces: value.workspaces,
layout: value.layout.clone(),
focused_window: value.focused_window,
workspace_layer: value.workspace_layer,
configuration_switcher,
}
}
@@ -136,6 +148,7 @@ pub struct Komorebi {
pub workspaces: Option<KomorebiWorkspacesConfig>,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
@@ -152,7 +165,7 @@ impl BarWidget for Komorebi {
let format = workspaces.display.unwrap_or(DisplayFormat::Text);
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, container_information)) in
for (i, (ws, container_information, _)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
if SelectableFrame::new(
@@ -279,6 +292,42 @@ impl BarWidget for Komorebi {
}
}
if let Some(layer_config) = &self.workspace_layer {
if layer_config.enable {
let layer = komorebi_notification_state
.workspaces
.iter()
.find(|o| komorebi_notification_state.selected_workspace.eq(&o.0))
.map(|(_, _, layer)| layer);
if let Some(layer) = layer {
let name = layer.to_string();
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(name).selectable(false)))
.clicked()
&& komorebi_client::send_batch([
SocketMessage::MouseFollowsFocus(false),
SocketMessage::ToggleWorkspaceLayer,
SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
),
])
.is_err()
{
tracing::error!(
"could not send the following batch of messages to komorebi:\n\
MouseFollowsFocus(false),
ToggleWorkspaceLayer,
MouseFollowsFocus({})",
komorebi_notification_state.mouse_follows_focus,
);
}
});
}
}
}
if let Some(layout_config) = &self.layout {
if layout_config.enable {
let workspace_idx: Option<usize> = komorebi_notification_state
@@ -474,7 +523,11 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>,
pub workspaces: Vec<(
String,
KomorebiNotificationStateContainerInformation,
WorkspaceLayer,
)>,
pub selected_workspace: String,
pub focused_container_information: KomorebiNotificationStateContainerInformation,
pub layout: KomorebiLayout,
@@ -483,6 +536,7 @@ pub struct KomorebiNotificationState {
pub work_area_offset: Option<Rect>,
pub stack_accent: Option<Color32>,
pub monitor_index: usize,
pub monitor_usr_idx_map: HashMap<usize, usize>,
}
impl KomorebiNotificationState {
@@ -494,7 +548,7 @@ impl KomorebiNotificationState {
pub fn handle_notification(
&mut self,
ctx: &Context,
monitor_index: usize,
monitor_index: Option<usize>,
notification: komorebi_client::Notification,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
@@ -552,6 +606,16 @@ impl KomorebiNotificationState {
},
}
self.monitor_usr_idx_map = notification.state.monitor_usr_idx_map.clone();
if monitor_index.is_none()
|| monitor_index.is_some_and(|idx| idx >= notification.state.monitors.elements().len())
{
// The bar's monitor is diconnected, so the bar is disabled no need to check anything
// any further otherwise we'll get `OutOfBounds` panics.
return;
}
let monitor_index = monitor_index.expect("should have a monitor index");
self.monitor_index = monitor_index;
self.mouse_follows_focus = notification.state.mouse_follows_focus;
@@ -570,11 +634,7 @@ impl KomorebiNotificationState {
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_show = if self.hide_empty_workspaces {
focused_workspace_idx == i
|| !ws.containers().is_empty()
|| !ws.floating_windows().is_empty()
|| ws.monocle_container().is_some()
|| ws.maximized_window().is_some()
focused_workspace_idx == i || !ws.is_empty()
} else {
true
};
@@ -583,6 +643,7 @@ impl KomorebiNotificationState {
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
ws.into(),
ws.layer().to_owned(),
));
}
}

View File

@@ -3,6 +3,7 @@ mod battery;
mod config;
mod cpu;
mod date;
mod keyboard;
mod komorebi;
mod komorebi_layout;
mod media;
@@ -37,12 +38,10 @@ use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId;
@@ -51,6 +50,7 @@ use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows_core::BOOL;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
@@ -237,12 +237,16 @@ fn main() -> color_eyre::Result<()> {
&SocketMessage::State,
)?)?;
let (monitor_index, work_area_offset) = match &config.monitor {
let (usr_monitor_index, work_area_offset) = match &config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
(monitor_config.index, monitor_config.work_area_offset)
}
MonitorConfigOrIndex::Index(idx) => (*idx, None),
};
let monitor_index = state
.monitor_usr_idx_map
.get(&usr_monitor_index)
.map_or(usr_monitor_index, |i| *i);
MONITOR_RIGHT.store(
state.monitors.elements()[monitor_index].size().right,
@@ -333,7 +337,6 @@ fn main() -> color_eyre::Result<()> {
tracing::info!("watching configuration file for changes");
let config_arc = Arc::new(config);
eframe::run_native(
"komorebi-bar",
native_options,
@@ -421,7 +424,7 @@ fn main() -> color_eyre::Result<()> {
}
});
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))

View File

@@ -148,7 +148,7 @@ impl Network {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"{: >width$}/s | ",
"{: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
@@ -159,14 +159,14 @@ impl Network {
),
),
NetworkReadingFormat::Total => (
format!("{} | ", reading.received_text),
format!("{} ", reading.received_text),
reading.transmitted_text,
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s | ",
"DOWN: {: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
@@ -177,7 +177,7 @@ impl Network {
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s | ", reading.received_text),
format!("\u{2211}DOWN: {}/s ", reading.received_text),
format!("\u{2211}UP: {}/s", reading.transmitted_text),
),
},

View File

@@ -4,6 +4,8 @@ use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::keyboard::Keyboard;
use crate::keyboard::KeyboardConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
@@ -34,6 +36,7 @@ pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
Date(DateConfig),
Keyboard(KeyboardConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
Memory(MemoryConfig),
@@ -49,6 +52,7 @@ impl WidgetConfig {
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
@@ -64,6 +68,7 @@ impl WidgetConfig {
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Keyboard(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)

View File

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

View File

@@ -2,10 +2,15 @@
#![allow(clippy::missing_errors_doc)]
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::animation::PerAnimationPrefixConfig;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::config_generation::IdWithIdentifier;
pub use komorebi::config_generation::IdWithIdentifierAndComment;
pub use komorebi::config_generation::MatchingRule;
pub use komorebi::config_generation::MatchingStrategy;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
@@ -15,6 +20,10 @@ pub use komorebi::core::Arrangement;
pub use komorebi::core::Axis;
pub use komorebi::core::BorderImplementation;
pub use komorebi::core::BorderStyle;
pub use komorebi::core::Column;
pub use komorebi::core::ColumnSplit;
pub use komorebi::core::ColumnSplitWithCapacity;
pub use komorebi::core::ColumnWidth;
pub use komorebi::core::CustomLayout;
pub use komorebi::core::CycleDirection;
pub use komorebi::core::DefaultLayout;
@@ -39,17 +48,26 @@ 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::workspace::WorkspaceLayer;
pub use komorebi::AnimationsConfig;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi::WindowContainerBehaviour;
pub use komorebi::WindowsApi;
pub use komorebi::WorkspaceConfig;
use komorebi::DATA_DIR;

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-gui"
version = "0.1.34"
version = "0.1.35"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -10,6 +10,7 @@ komorebi-client = { path = "../komorebi-client" }
eframe = { workspace = true }
egui_extras = { workspace = true }
random_word = { version = "0.4.3", features = ["en"] }
random_word = { version = "0.4", features = ["en"] }
serde_json = { workspace = true }
windows-core = { workspace = true }
windows = { workspace = true }

View File

@@ -215,7 +215,7 @@ impl KomorebiGui {
extern "system" fn enum_window(
hwnd: windows::Win32::Foundation::HWND,
lparam: windows::Win32::Foundation::LPARAM,
) -> windows::Win32::Foundation::BOOL {
) -> windows_core::BOOL {
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
let window = Window::from(hwnd.0 as isize);

View File

@@ -1,14 +1,14 @@
[package]
name = "komorebi-themes"
version = "0.1.34"
version = "0.1.35"
edition = "2021"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "24362c4" }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "911079d" }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f85cc3c", default-features = false, features = ["egui30"] }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
eframe = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_variant = "0.1"
strum = "0.26"
strum = "0.26"

View File

@@ -4,6 +4,7 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::IntoEnumIterator;
pub use base16_egui_themes::Base16;
@@ -11,7 +12,7 @@ pub use catppuccin_egui;
pub use eframe::egui::Color32;
use serde_variant::to_variant_name;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "type")]
pub enum Theme {
/// A theme from catppuccin-egui
@@ -48,7 +49,7 @@ impl Theme {
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
pub enum Base16Value {
Base00,
Base01,
@@ -92,7 +93,7 @@ impl Base16Value {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
pub enum Catppuccin {
Frappe,
Latte,
@@ -117,7 +118,7 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
pub enum CatppuccinValue {
Rosewater,
Flamingo,

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.34"
version = "0.1.35"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
@@ -25,7 +25,7 @@ lazy_static = { workspace = true }
miow = "0.6"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.8"
os_info = "3.10"
parking_lot = "0.12"
paste = { workspace = true }
regex = "1"
@@ -44,6 +44,7 @@ which = { workspace = true }
win32-display-data = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }
windows-numerics = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"

View File

@@ -22,7 +22,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum PerAnimationPrefixConfig<T> {
Prefix(HashMap<AnimationPrefix, T>),

View File

@@ -1,4 +1,5 @@
use crate::border_manager::window_kind_colour;
use crate::border_manager::RenderTarget;
use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
@@ -17,8 +18,6 @@ use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::LazyLock;
use std::sync::OnceLock;
use windows::Foundation::Numerics::Matrix3x2;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::FALSE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
@@ -32,7 +31,6 @@ use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
@@ -68,13 +66,29 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows_core::BOOL;
use windows_core::PCWSTR;
use windows_numerics::Matrix3x2;
pub struct RenderFactory(ID2D1Factory);
unsafe impl Sync for RenderFactory {}
unsafe impl Send for RenderFactory {}
impl Deref for RenderFactory {
type Target = ID2D1Factory;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[allow(clippy::expect_used)]
static RENDER_FACTORY: LazyLock<ID2D1Factory> = unsafe {
static RENDER_FACTORY: LazyLock<RenderFactory> = unsafe {
LazyLock::new(|| {
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
.expect("creating RENDER_FACTORY failed")
RenderFactory(
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
.expect("creating RENDER_FACTORY failed"),
)
})
};
@@ -100,7 +114,7 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
#[derive(Debug, Clone)]
pub struct Border {
pub hwnd: isize,
pub render_target: OnceLock<ID2D1HwndRenderTarget>,
pub render_target: OnceLock<RenderTarget>,
pub tracking_hwnd: isize,
pub window_rect: Rect,
pub window_kind: WindowKind,
@@ -180,7 +194,7 @@ impl Border {
loop {
unsafe {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
tracing::debug!("border window event processing thread shutdown");
break;
};
@@ -261,7 +275,11 @@ impl Border {
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
if border.render_target.set(render_target.clone()).is_err() {
if border
.render_target
.set(RenderTarget(render_target.clone()))
.is_err()
{
return Err(anyhow!("could not store border render target"));
}
@@ -275,7 +293,7 @@ impl Border {
};
let mut render_targets = RENDER_TARGETS.lock();
render_targets.insert(border.hwnd, render_target);
render_targets.insert(border.hwnd, RenderTarget(render_target));
Ok(border.clone())
},
Err(error) => Err(error.into()),
@@ -300,7 +318,7 @@ impl Border {
// this triggers WM_PAINT in the callback below
pub fn invalidate(&self) {
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
let _ = unsafe { InvalidateRect(Option::from(self.hwnd()), None, false) };
}
pub extern "system" fn callback(
@@ -508,7 +526,7 @@ impl Border {
}
}
}
let _ = ValidateRect(window, None);
let _ = ValidateRect(Option::from(window), None);
LRESULT(0)
}
WM_DESTROY => {

View File

@@ -23,12 +23,14 @@ use serde::Deserialize;
use serde::Serialize;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use strum::Display;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
@@ -56,8 +58,19 @@ lazy_static! {
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
static ref RENDER_TARGETS: Mutex<HashMap<isize, ID2D1HwndRenderTarget>> =
Mutex::new(HashMap::new());
static ref RENDER_TARGETS: Mutex<HashMap<isize, RenderTarget>> = Mutex::new(HashMap::new());
}
#[derive(Debug, Clone)]
pub struct RenderTarget(pub ID2D1HwndRenderTarget);
unsafe impl Send for RenderTarget {}
impl Deref for RenderTarget {
type Target = ID2D1HwndRenderTarget;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct Notification(pub Option<isize>);
@@ -211,6 +224,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?;
}
for window in ws.floating_windows() {
let mut window_kind = WindowKind::Unfocused;
if foreground_window == window.hwnd {
window_kind = WindowKind::Floating;
}
window.set_accent(window_kind_colour(window_kind))?;
}
}
}
}
@@ -251,6 +274,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.map(|b| b.window_kind == WindowKind::Floating)
.unwrap_or_default())
});
if !should_process_notification && switch_focus_to_from_floating_window {
should_process_notification = true;
}
@@ -488,7 +512,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Some(last_focus_state) => last_focus_state != new_focus_state,
};
if new_border {
if new_border || should_invalidate {
border.set_position(&rect, reference_hwnd)?;
}
@@ -568,7 +592,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Ok(())
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, JsonSchema, PartialEq)]
pub enum ZOrder {
Top,
NoTopMost,

View File

@@ -8,7 +8,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum Colour {
/// Colour represented as RGB
@@ -51,7 +51,7 @@ impl From<Colour> for Color32 {
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hex(HexColor);
impl JsonSchema for Hex {
@@ -78,7 +78,7 @@ impl From<Colour> for u32 {
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct Rgb {
/// Red
pub r: u32,
@@ -120,8 +120,8 @@ impl From<u32> for Rgb {
fn from(value: u32) -> Self {
Self {
r: value & 0xff,
g: value >> 8 & 0xff,
b: value >> 16 & 0xff,
g: (value >> 8) & 0xff,
b: (value >> 16) & 0xff,
}
}
}

View File

@@ -13,11 +13,11 @@ use windows::core::HRESULT;
use windows::core::HSTRING;
use windows::core::PCWSTR;
use windows::core::PWSTR;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::SIZE;
use windows::Win32::UI::Shell::Common::IObjectArray;
use windows_core::BOOL;
type DesktopID = GUID;

View File

@@ -6,7 +6,16 @@ use strum::Display;
use strum::EnumString;
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
pub enum AnimationStyle {
Linear,

View File

@@ -75,7 +75,7 @@ pub struct IdWithIdentifier {
pub matching_strategy: Option<MatchingStrategy>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, JsonSchema)]
pub enum MatchingStrategy {
Legacy,
Equals,

View File

@@ -19,6 +19,10 @@ use crate::KomorebiTheme;
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
pub use custom_layout::Column;
pub use custom_layout::ColumnSplit;
pub use custom_layout::ColumnSplitWithCapacity;
pub use custom_layout::ColumnWidth;
pub use custom_layout::CustomLayout;
pub use cycle_direction::CycleDirection;
pub use default_layout::DefaultLayout;
@@ -119,6 +123,7 @@ pub enum SocketMessage {
CycleFocusMonitor(CycleDirection),
CycleFocusWorkspace(CycleDirection),
FocusMonitorNumber(usize),
FocusMonitorAtCursor,
FocusLastWorkspace,
CloseWorkspace,
FocusWorkspaceNumber(usize),
@@ -144,6 +149,7 @@ pub enum SocketMessage {
NamedWorkspaceLayoutCustomRule(String, usize, PathBuf),
ClearWorkspaceLayoutRules(usize, usize),
ClearNamedWorkspaceLayoutRules(String),
ToggleWorkspaceLayer,
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
@@ -180,6 +186,7 @@ pub enum SocketMessage {
StackbarFontFamily(Option<String>),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
ToggleWindowBasedWorkAreaOffset,
ResizeDelta(i32),
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
@@ -431,7 +438,16 @@ pub enum MoveBehaviour {
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
pub enum CrossBoundaryBehaviour {
/// Attempt to perform actions across a workspace boundary
@@ -441,7 +457,16 @@ pub enum CrossBoundaryBehaviour {
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
pub enum HidingBehaviour {
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)

View File

@@ -182,7 +182,7 @@ lazy_static! {
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
Arc::new(Mutex::new(HidingBehaviour::Minimize));
Arc::new(Mutex::new(HidingBehaviour::Cloak));
pub static ref HOME_DIR: PathBuf = {
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
let home = PathBuf::from(&home_path);

View File

@@ -176,7 +176,7 @@ fn main() -> Result<()> {
let session_id = WindowsApi::process_id_to_session_id()?;
SESSION_ID.store(session_id, Ordering::SeqCst);
let mut system = sysinfo::System::new_all();
let mut system = sysinfo::System::new();
system.refresh_processes(ProcessesToUpdate::All, true);
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
@@ -268,8 +268,14 @@ fn main() -> Result<()> {
let dumped_state = temp_dir().join("komorebi.state.json");
if !opts.clean_state && dumped_state.is_file() {
let state: State = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?)?;
wm.lock().apply_state(state);
if let Ok(state) = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?) {
wm.lock().apply_state(state);
} else {
tracing::warn!(
"cannot apply state from {}; state struct is not up to date",
dumped_state.display()
);
}
}
wm.lock().retile_all(false)?;
@@ -285,7 +291,7 @@ fn main() -> Result<()> {
transparency_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());
reaper::listen_for_notifications(wm.clone());
focus_manager::listen_for_notifications(wm.clone());
theme_manager::listen_for_notifications();

View File

@@ -36,31 +36,31 @@ use crate::WindowsApi;
)]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
id: isize,
pub id: isize,
#[getset(get = "pub", set = "pub")]
name: String,
pub name: String,
#[getset(get = "pub", set = "pub")]
device: String,
pub device: String,
#[getset(get = "pub", set = "pub")]
device_id: String,
pub device_id: String,
#[getset(get = "pub", set = "pub")]
serial_number_id: Option<String>,
pub serial_number_id: Option<String>,
#[getset(get = "pub", set = "pub")]
size: Rect,
pub size: Rect,
#[getset(get = "pub", set = "pub")]
work_area_size: Rect,
pub work_area_size: Rect,
#[getset(get_copy = "pub", set = "pub")]
work_area_offset: Option<Rect>,
pub work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
window_based_work_area_offset: Option<Rect>,
pub window_based_work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
window_based_work_area_offset_limit: isize,
workspaces: Ring<Workspace>,
pub window_based_work_area_offset_limit: isize,
pub workspaces: Ring<Workspace>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
last_focused_workspace: Option<usize>,
pub last_focused_workspace: Option<usize>,
#[getset(get_mut = "pub")]
workspace_names: HashMap<usize, String>,
pub workspace_names: HashMap<usize, String>,
}
impl_ring_elements!(Monitor, Workspace);
@@ -118,6 +118,26 @@ pub fn new(
}
impl Monitor {
pub fn new(
id: isize,
size: Rect,
work_area_size: Rect,
name: String,
device: String,
device_id: String,
serial_number_id: Option<String>,
) -> Self {
new(
id,
size,
work_area_size,
name,
device,
device_id,
serial_number_id,
)
}
pub fn placeholder() -> Self {
Self {
id: 0,

View File

@@ -2,21 +2,33 @@ use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::System::Power::POWERBROADCAST_SETTING;
use windows::Win32::System::SystemServices::GUID_LIDSWITCH_STATE_CHANGE;
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_CONFIGCHANGED;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEARRIVAL;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEREMOVECOMPLETE;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVTYP_DEVICEINTERFACE;
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
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::PBT_POWERSETTINGCHANGE;
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
@@ -75,7 +87,7 @@ impl Hidden {
loop {
unsafe {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
tracing::debug!("hidden window event processing thread shutdown");
break;
};
@@ -92,8 +104,57 @@ impl Hidden {
let hwnd = hwnd_receiver.recv()?;
// Register Session Lock/Unlock events
WindowsApi::wts_register_session_notification(hwnd)?;
// Register Laptop lid open/close events
WindowsApi::register_power_setting_notification(
hwnd,
&GUID_LIDSWITCH_STATE_CHANGE,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
// Register device interface events for multiple display related devices. Some of this
// device interfaces might not be needed but it doesn't hurt to have them in case some user
// uses some output device as monitor that falls into one of these device interface class
// GUID.
let monitor_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
dbcc_reserved: 0,
dbcc_classguid: GUID_DEVINTERFACE_MONITOR,
dbcc_name: [0; 1],
};
let display_adapter_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
dbcc_reserved: 0,
dbcc_classguid: GUID_DEVINTERFACE_DISPLAY_ADAPTER,
dbcc_name: [0; 1],
};
let video_output_filter = DEV_BROADCAST_DEVICEINTERFACE_W {
dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
dbcc_reserved: 0,
dbcc_classguid: GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL,
dbcc_name: [0; 1],
};
WindowsApi::register_device_notification(
hwnd,
monitor_filter,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
WindowsApi::register_device_notification(
hwnd,
display_adapter_filter,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
WindowsApi::register_device_notification(
hwnd,
video_output_filter,
REGISTER_NOTIFICATION_FLAGS(0),
)?;
Ok(Self { hwnd })
}
@@ -128,6 +189,35 @@ impl Hidden {
);
LRESULT(0)
}
// Monitor change power status
PBT_POWERSETTINGCHANGE => {
if let POWERBROADCAST_SETTING {
PowerSetting: GUID_LIDSWITCH_STATE_CHANGE,
DataLength: _,
Data: [0],
} = *(lparam.0 as *const POWERBROADCAST_SETTING)
{
tracing::debug!(
"WM_POWERBROADCAST event received - laptop lid closed"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
);
} else if let POWERBROADCAST_SETTING {
PowerSetting: GUID_LIDSWITCH_STATE_CHANGE,
DataLength: _,
Data: [1],
} = *(lparam.0 as *const POWERBROADCAST_SETTING)
{
tracing::debug!(
"WM_POWERBROADCAST event received - laptop lid opened"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
);
}
LRESULT(0)
}
_ => LRESULT(0),
}
}
@@ -188,10 +278,15 @@ impl Hidden {
// Original idea from https://stackoverflow.com/a/33762334
WM_DEVICECHANGE => {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
let event = wparam.0 as u32;
if event == DBT_DEVNODES_CHANGED
|| event == DBT_CONFIGCHANGED
|| event == DBT_DEVICEARRIVAL
|| event == DBT_DEVICEREMOVECOMPLETE
{
tracing::debug!(
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
);
"WM_DEVICECHANGE event received with one of [DBT_DEVNODES_CHANGED, DBT_CONFIGCHANGED, DBT_DEVICEARRIVAL, DBT_DEVICEREMOVECOMPLETE] - display added or removed"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::DisplayConnectionChange,
);

View File

@@ -1,17 +1,18 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::Rect;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
use crate::notify_subscribers;
use crate::MonitorConfig;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::WindowManager;
use crate::WindowsApi;
use crate::WORKSPACE_MATCHING_RULES;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
@@ -44,10 +45,10 @@ static ACTIVE: AtomicBool = AtomicBool::new(true);
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = OnceLock::new();
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, Monitor>>> = OnceLock::new();
pub fn channel() -> &'static (Sender<MonitorNotification>, Receiver<MonitorNotification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
}
fn event_tx() -> Sender<MonitorNotification> {
@@ -64,12 +65,12 @@ pub fn send_notification(notification: MonitorNotification) {
}
}
pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
monitor_cache.insert(device_id.to_string(), config);
monitor_cache.insert(serial_or_device_id.to_string(), monitor);
}
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
@@ -135,21 +136,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let receiver = event_rx();
'receiver: for notification in receiver {
if !ACTIVE.load_consume() {
if matches!(
if !ACTIVE.load_consume()
&& matches!(
notification,
MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked
) {
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
);
)
{
tracing::debug!(
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
);
ACTIVE.store(true, Ordering::SeqCst);
border_manager::send_notification(None);
}
continue 'receiver;
ACTIVE.store(true, Ordering::SeqCst);
border_manager::send_notification(None);
}
let mut wm = wm.lock();
@@ -163,10 +162,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
);
ACTIVE.store(false, Ordering::SeqCst);
}
MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked => {
// this is only handled above if the reconciliator is paused
}
MonitorNotification::WorkAreaChanged => {
tracing::debug!("handling work area changed notification");
let offset = wm.work_area_offset;
@@ -246,7 +241,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
}
}
MonitorNotification::DisplayConnectionChange => {
// this is handled above if the reconciliator is paused but we should still check if
// there were any changes to the connected monitors while the system was
// suspended/locked.
MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked
| MonitorNotification::DisplayConnectionChange => {
tracing::debug!("handling display connection change notification");
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
@@ -260,8 +260,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// 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()) {
if attached.serial_number_id().eq(monitor.serial_number_id())
|| attached.device_id().eq(monitor.device_id())
{
monitor.set_id(attached.id());
monitor.set_device_id(attached.device_id().clone());
monitor.set_serial_number_id(attached.serial_number_id().clone());
monitor.set_name(attached.name().clone());
monitor.set_size(*attached.size());
monitor.set_work_area_size(*attached.work_area_size());
@@ -271,6 +275,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if initial_monitor_count == attached_devices.len() {
tracing::debug!("monitor counts match, reconciliation not required");
drop(wm);
continue 'receiver;
}
@@ -278,6 +283,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
tracing::debug!(
"no devices found, skipping reconciliation to avoid breaking state"
);
drop(wm);
continue 'receiver;
}
@@ -287,41 +293,108 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
attached_devices.len()
);
// Gather all the containers that will be orphaned from disconnected and invalid displays
let mut orphaned_containers = vec![];
// Windows to remove from `known_hwnds`
let mut windows_to_remove = Vec::new();
// 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());
for (m_idx, m) in wm.monitors().iter().enumerate() {
if !attached_devices.iter().any(|attached| {
attached.serial_number_id().eq(m.serial_number_id())
|| attached.device_id().eq(m.device_id())
}) {
let id = m
.serial_number_id()
.as_ref()
.map_or(m.device_id().clone(), |sn| sn.clone());
newly_removed_displays.push(id.clone());
let focused_workspace_idx = m.focused_workspace_idx();
for (idx, workspace) in m.workspaces().iter().enumerate() {
let is_focused_workspace = idx == focused_workspace_idx;
let focused_container_idx = workspace.focused_container_idx();
for (c_idx, container) in workspace.containers().iter().enumerate()
{
let focused_window_idx = container.focused_window_idx();
for (w_idx, window) in container.windows().iter().enumerate() {
windows_to_remove.push(window.hwnd);
if is_focused_workspace
&& c_idx == focused_container_idx
&& w_idx == focused_window_idx
{
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if window.is_focused() {
window.minimize();
}
}
}
}
if let Some(maximized) = workspace.maximized_window() {
windows_to_remove.push(maximized.hwnd);
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if maximized.is_focused() {
maximized.minimize();
}
}
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
windows_to_remove.push(window.hwnd);
}
if let Some(window) = container.focused_window() {
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if window.is_focused() {
window.minimize();
}
}
}
for window in workspace.floating_windows() {
windows_to_remove.push(window.hwnd);
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
if window.is_focused() {
window.minimize();
}
}
}
// Remove any workspace_rules for this specific monitor
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
let mut rules_to_remove = Vec::new();
for (i, rule) in workspace_rules.iter().enumerate().rev() {
if rule.monitor_index == m_idx {
rules_to_remove.push(i);
}
}
for i in rules_to_remove {
workspace_rules.remove(i);
}
// Let's add their state to the cache for later
monitor_cache.insert(m.device_id().clone(), m.into());
monitor_cache.insert(id, m.clone());
}
}
if !orphaned_containers.is_empty() {
tracing::info!(
"removed orphaned containers from: {newly_removed_displays:?}"
);
}
// Update known_hwnds
wm.known_hwnds.retain(|i, _| !windows_to_remove.contains(i));
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()));
wm.monitors_mut().retain(|m| {
!newly_removed_displays.iter().any(|id| {
m.serial_number_id().as_ref().is_some_and(|sn| sn == id)
|| m.device_id() == id
})
});
}
let post_removal_monitor_count = wm.monitors().len();
@@ -330,24 +403,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
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_to_back(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() {
@@ -360,7 +415,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let post_removal_monitor_count = wm.monitors().len();
// This is the list of device ids after we have removed detached displays
// This is the list of device ids after we have removed detached displays. We can
// keep this with just the device_ids without the serial numbers since this is used
// only to check which one is the newly added monitor below if there is a new
// monitor. Everything done after with said new monitor will again consider both
// serial number and device ids.
let post_removal_device_ids = wm
.monitors()
.iter()
@@ -370,7 +429,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// 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)?;
WindowsApi::load_monitor_information(&mut wm)?;
let post_addition_monitor_count = wm.monitors().len();
@@ -379,42 +438,181 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
"monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors",
);
let known_hwnds = wm.known_hwnds.clone();
let offset = wm.work_area_offset;
let mouse_follows_focus = wm.mouse_follows_focus;
let focused_monitor_idx = wm.focused_monitor_idx();
let focused_workspace_idx = wm.focused_workspace_idx()?;
// Look in the updated state for new monitors
for m in wm.monitors_mut() {
let device_id = m.device_id().clone();
for (i, m) in wm.monitors_mut().iter_mut().enumerate() {
let device_id = m.device_id();
// We identify a new monitor when we encounter a new device id
if !post_removal_device_ids.contains(&device_id) {
if !post_removal_device_ids.contains(device_id) {
let mut cache_hit = false;
let mut cached_id = String::new();
// Check if that device id exists in the cache for this session
if let Some(cached) = monitor_cache.get(&device_id) {
if let Some((id, cached)) = monitor_cache.get_key_value(device_id).or(m
.serial_number_id()
.as_ref()
.and_then(|sn| monitor_cache.get_key_value(sn)))
{
cache_hit = true;
cached_id = id.clone();
tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying");
tracing::info!("found monitor and workspace configuration for {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),
);
// If it does, load the monitor removing any window that has since
// been closed or moved to another workspace
*m = cached.clone();
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)?;
let focused_workspace_idx = m.focused_workspace_idx();
for (j, workspace) in m.workspaces_mut().iter_mut().enumerate() {
// If this is the focused workspace we need to show (restore) all
// windows that were visible since they were probably minimized by
// Windows.
let is_focused_workspace = j == focused_workspace_idx;
let focused_container_idx = workspace.focused_container_idx();
let mut empty_containers = Vec::new();
for (idx, container) in
workspace.containers_mut().iter_mut().enumerate()
{
container.windows_mut().retain(|window| {
window.exe().is_ok()
&& !known_hwnds.contains_key(&window.hwnd)
});
if container.windows().is_empty() {
empty_containers.push(idx);
}
if is_focused_workspace {
if let Some(window) = container.focused_window() {
tracing::debug!(
"restoring window: {}",
window.hwnd
);
WindowsApi::restore_window(window.hwnd);
} else {
// If the focused window was moved or removed by
// the user after the disconnect then focus the
// first window and show that one
container.focus_window(0);
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
}
}
}
}
// Remove empty containers
for empty_idx in empty_containers {
if empty_idx == focused_container_idx {
workspace.remove_container(empty_idx);
} else {
workspace.remove_container_by_idx(empty_idx);
}
}
if let Some(window) = workspace.maximized_window() {
if window.exe().is_err()
|| known_hwnds.contains_key(&window.hwnd)
{
workspace.set_maximized_window(None);
} else if is_focused_workspace {
WindowsApi::restore_window(window.hwnd);
}
}
if let Some(container) = workspace.monocle_container_mut() {
container.windows_mut().retain(|window| {
window.exe().is_ok()
&& !known_hwnds.contains_key(&window.hwnd)
});
if container.windows().is_empty() {
workspace.set_monocle_container(None);
} else if is_focused_workspace {
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
} else {
// If the focused window was moved or removed by
// the user after the disconnect then focus the
// first window and show that one
container.focus_window(0);
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
}
}
}
}
workspace.floating_windows_mut().retain(|window| {
window.exe().is_ok()
&& !known_hwnds.contains_key(&window.hwnd)
});
if is_focused_workspace {
for window in workspace.floating_windows() {
WindowsApi::restore_window(window.hwnd);
}
}
// Apply workspace rules
let mut workspace_matching_rules =
WORKSPACE_MATCHING_RULES.lock();
if let Some(rules) = workspace
.workspace_config()
.as_ref()
.and_then(|c| c.workspace_rules.as_ref())
{
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: false,
});
}
}
if let Some(rules) = workspace
.workspace_config()
.as_ref()
.and_then(|c| c.initial_workspace_rules.as_ref())
{
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
monitor_index: i,
workspace_index: j,
matching_rule: r.clone(),
initial_only: true,
});
}
}
}
// Restore windows from new monitor and update the focused
// workspace
m.load_focused_workspace(mouse_follows_focus)?;
m.update_focused_workspace(offset)?;
}
// 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);
if cache_hit && !cached_id.is_empty() {
monitor_cache.remove(&cached_id);
}
}
}
// Refocus the previously focused monitor since the code above might
// steal the focus away.
wm.focus_monitor(focused_monitor_idx)?;
wm.focus_workspace(focused_workspace_idx)?;
}
let final_count = wm.monitors().len();

View File

@@ -66,6 +66,7 @@ use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::WorkspaceLayer;
use crate::workspace::WorkspaceWindowLocation;
use crate::GlobalState;
use crate::Notification;
@@ -291,13 +292,37 @@ impl WindowManager {
}
}
SocketMessage::FocusWindow(direction) => {
self.focus_container_in_direction(direction)?;
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer() {
WorkspaceLayer::Tiling => {
self.focus_container_in_direction(direction)?;
}
WorkspaceLayer::Floating => {
self.focus_floating_window_in_direction(direction)?;
}
}
}
SocketMessage::MoveWindow(direction) => {
self.move_container_in_direction(direction)?;
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer() {
WorkspaceLayer::Tiling => {
self.move_container_in_direction(direction)?;
}
WorkspaceLayer::Floating => {
self.move_floating_window_in_direction(direction)?;
}
}
}
SocketMessage::CycleFocusWindow(direction) => {
self.focus_container_in_cycle_direction(direction)?;
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer() {
WorkspaceLayer::Tiling => {
self.focus_container_in_cycle_direction(direction)?;
}
WorkspaceLayer::Floating => {
self.focus_floating_window_in_cycle_direction(direction)?;
}
}
}
SocketMessage::CycleMoveWindow(direction) => {
self.move_container_in_cycle_direction(direction)?;
@@ -735,6 +760,11 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::FocusMonitorAtCursor => {
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
}
}
SocketMessage::Retile => {
border_manager::destroy_all_borders()?;
self.retile_all(false)?
@@ -848,7 +878,15 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
let focused_monitor = self
@@ -871,7 +909,15 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
let mut can_close = false;
@@ -907,7 +953,15 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
let idx = self
@@ -930,7 +984,15 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
if self.focused_workspace_idx().unwrap_or_default() != workspace_idx {
@@ -942,7 +1004,15 @@ impl WindowManager {
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
let focused_monitor_idx = self.focused_monitor_idx();
@@ -975,6 +1045,29 @@ impl WindowManager {
self.focus_workspace(workspace_idx)?;
}
}
SocketMessage::ToggleWorkspaceLayer => {
let mouse_follows_focus = self.mouse_follows_focus;
let workspace = self.focused_workspace_mut()?;
match workspace.layer() {
WorkspaceLayer::Tiling => {
workspace.set_layer(WorkspaceLayer::Floating);
if let Some(first) = workspace.floating_windows().first() {
first.focus(mouse_follows_focus)?;
}
}
WorkspaceLayer::Floating => {
workspace.set_layer(WorkspaceLayer::Tiling);
if let Some(container) = workspace.focused_container() {
if let Some(window) = container.focused_window() {
window.focus(mouse_follows_focus)?;
}
}
}
};
}
SocketMessage::Stop => {
self.stop(false)?;
}
@@ -1413,6 +1506,14 @@ impl WindowManager {
self.retile_all(false)?;
}
}
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
let workspace = self.focused_workspace_mut()?;
workspace.set_apply_window_based_work_area_offset(
!workspace.apply_window_based_work_area_offset(),
);
self.retile_all(false)?;
}
SocketMessage::QuickSave => {
let workspace = self.focused_workspace()?;
let resize = workspace.resize_dimensions();
@@ -1782,6 +1883,9 @@ impl WindowManager {
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
};
// Update list of known_hwnds and their monitor/workspace index pair
self.update_known_hwnds();
notify_subscribers(
Notification {
event: NotificationEvent::Socket(message.clone()),

View File

@@ -1,4 +1,3 @@
use std::fs::OpenOptions;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
@@ -33,7 +32,6 @@ use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::DATA_DIR;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
@@ -254,7 +252,12 @@ impl WindowManager {
already_moved_window_handles.remove(&window.hwnd);
}
WindowManagerEvent::FocusChange(_, window) => {
self.update_focused_workspace(self.mouse_follows_focus, false)?;
// don't want to trigger the full workspace updates when there are no managed
// containers - this makes floating windows on empty workspaces go into very
// annoying focus change loops which prevents users from interacting with them
if !self.focused_workspace()?.containers().is_empty() {
self.update_focused_workspace(self.mouse_follows_focus, false)?;
}
let workspace = self.focused_workspace_mut()?;
let floating_window_idx = workspace
@@ -302,30 +305,28 @@ impl WindowManager {
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) && 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((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
if focused_pair != (*m_idx, *w_idx) {
// 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();
}
}
workspace_reconciliator::send_notification(i, j);
needs_reconciliation = true;
}
workspace_reconciliator::send_notification(*m_idx, *w_idx);
needs_reconciliation = true;
}
}
@@ -336,11 +337,14 @@ impl WindowManager {
// 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.contains_window(window.hwnd)
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
if let Some(focused_workspace_idx) = self
.monitors()
.get(*m_idx)
.map(|m| m.focused_workspace_idx())
{
if *m_idx != self.focused_monitor_idx()
&& *w_idx != focused_workspace_idx
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
@@ -499,15 +503,9 @@ impl WindowManager {
// This will be true if we have moved to another monitor
let mut moved_across_monitors = false;
for (i, monitors) in self.monitors().iter().enumerate() {
for workspace in monitors.workspaces() {
if workspace.contains_window(window.hwnd) && i != target_monitor_idx {
moved_across_monitors = true;
break;
}
}
if moved_across_monitors {
break;
if let Some((m_idx, _)) = self.known_hwnds.get(&window.hwnd) {
if *m_idx != target_monitor_idx {
moved_across_monitors = true;
}
}
@@ -711,40 +709,8 @@ impl WindowManager {
window.center(&self.focused_monitor_work_area()?)?;
}
tracing::trace!("updating list of known hwnds");
let mut known_hwnds = vec![];
for monitor in self.monitors() {
for workspace in monitor.workspaces() {
for container in workspace.containers() {
for window in container.windows() {
known_hwnds.push(window.hwnd);
}
}
for window in workspace.floating_windows() {
known_hwnds.push(window.hwnd);
}
if let Some(window) = workspace.maximized_window() {
known_hwnds.push(window.hwnd);
}
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
known_hwnds.push(window.hwnd);
}
}
}
}
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &known_hwnds)?;
// Update list of known_hwnds and their monitor/workspace index pair
self.update_known_hwnds();
notify_subscribers(
Notification {

View File

@@ -1,14 +1,164 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::notify_subscribers;
use crate::winevent::WinEvent;
use crate::NotificationEvent;
use crate::Window;
use crate::WindowManager;
use crate::WindowManagerEvent;
use crate::DATA_DIR;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
lazy_static! {
pub static ref HWNDS_CACHE: Arc<Mutex<HashMap<isize, (usize, usize)>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub struct ReaperNotification(pub HashMap<isize, (usize, usize)>);
static CHANNEL: OnceLock<(Sender<ReaperNotification>, Receiver<ReaperNotification>)> =
OnceLock::new();
pub fn channel() -> &'static (Sender<ReaperNotification>, Receiver<ReaperNotification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
}
fn event_tx() -> Sender<ReaperNotification> {
channel().0.clone()
}
fn event_rx() -> Receiver<ReaperNotification> {
channel().1.clone()
}
pub fn send_notification(hwnds: HashMap<isize, (usize, usize)>) {
if event_tx().try_send(ReaperNotification(hwnds)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
watch_for_orphans(wm.clone());
std::thread::spawn(move || loop {
match find_orphans(wm.clone()) {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
for notification in receiver {
let orphan_hwnds = notification.0;
let mut wm = wm.lock();
let offset = wm.work_area_offset;
let mut update_borders = false;
for (hwnd, (m_idx, w_idx)) in orphan_hwnds.iter() {
if let Some(monitor) = wm.monitors_mut().get_mut(*m_idx) {
let focused_workspace_idx = monitor.focused_workspace_idx();
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
};
if let Some(workspace) = monitor.workspaces_mut().get_mut(*w_idx) {
// Remove orphan window
if let Err(error) = workspace.remove_window(*hwnd) {
tracing::warn!(
"error reaping orphan window ({}) on monitor: {}, workspace: {}. Error: {}",
hwnd,
m_idx,
w_idx,
error,
);
}
if focused_workspace_idx == *w_idx {
// If this is not a focused workspace there is no need to update the
// workspace or the borders. That will already be done when the user
// changes to this workspace.
workspace.update(&work_area, offset, window_based_work_area_offset)?;
update_borders = true;
}
tracing::info!(
"reaped orphan window ({}) on monitor: {}, workspace: {}",
hwnd,
m_idx,
w_idx,
);
}
}
wm.known_hwnds.remove(hwnd);
let window = Window::from(*hwnd);
notify_subscribers(
crate::Notification {
event: NotificationEvent::WindowManager(WindowManagerEvent::Destroy(
WinEvent::ObjectDestroy,
window,
)),
state: wm.as_ref().into(),
},
true,
)?;
}
if update_borders {
border_manager::send_notification(None);
}
// Save to file
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(hwnd_json)?;
serde_json::to_writer_pretty(&file, &wm.known_hwnds.keys().collect::<Vec<_>>())?;
}
Ok(())
}
fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
// Cache current hwnds
{
let mut cache = HWNDS_CACHE.lock();
*cache = wm.lock().known_hwnds.clone();
}
std::thread::spawn(move || loop {
match find_orphans() {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
@@ -23,50 +173,37 @@ pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
});
}
pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
fn find_orphans() -> color_eyre::Result<()> {
tracing::info!("watching");
let arc = wm.clone();
loop {
std::thread::sleep(Duration::from_secs(1));
std::thread::sleep(Duration::from_millis(20));
let mut wm = arc.lock();
let offset = wm.work_area_offset;
let mut cache = HWNDS_CACHE.lock();
let mut orphan_hwnds = HashMap::new();
let mut update_borders = false;
for (hwnd, (m_idx, w_idx)) in cache.iter() {
let window = Window::from(*hwnd);
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)?;
update_borders = true;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
reaped_orphans.1,
i,
j
);
}
if !window.is_window()
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
// docs is closed
//
// I hate every single person who worked on Microsoft Office 365, especially Word
|| !window.is_visible()
{
orphan_hwnds.insert(window.hwnd, (*m_idx, *w_idx));
}
}
if update_borders {
border_manager::send_notification(None);
if !orphan_hwnds.is_empty() {
// Update reaper cache
cache.retain(|h, _| !orphan_hwnds.contains_key(h));
// Send handles to remove
event_tx().send(ReaperNotification(orphan_hwnds))?;
}
}
}

View File

@@ -123,7 +123,7 @@ impl Stackbar {
0,
None,
None,
HINSTANCE(windows_api::as_ptr!(instance)),
Option::from(HINSTANCE(windows_api::as_ptr!(instance))),
None,
)?;
@@ -133,7 +133,7 @@ impl Stackbar {
let mut msg: MSG = MSG::default();
loop {
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
tracing::debug!("stackbar window event processing thread shutdown");
break;
};
@@ -183,13 +183,13 @@ impl Stackbar {
WindowsApi::position_window(self.hwnd, &layout, false)?;
unsafe {
let hdc = GetDC(self.hwnd());
let hdc = GetDC(Option::from(self.hwnd()));
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
let hbrush = CreateSolidBrush(COLORREF(background));
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
SelectObject(hdc, hpen.into());
SelectObject(hdc, hbrush.into());
SetBkColor(hdc, COLORREF(background));
let mut logfont = LOGFONTW {
@@ -209,14 +209,14 @@ impl Stackbar {
let logical_height = -MulDiv(
STACKBAR_FONT_SIZE.load(Ordering::SeqCst),
72,
GetDeviceCaps(hdc, LOGPIXELSY),
GetDeviceCaps(Option::from(hdc), LOGPIXELSY),
);
logfont.lfHeight = logical_height;
let hfont = CreateFontIndirectW(&logfont);
SelectObject(hdc, hfont);
SelectObject(hdc, hfont.into());
for (i, window) in container.windows().iter().enumerate() {
if window.hwnd == container.focused_window().copied().unwrap_or_default().hwnd {
@@ -283,13 +283,13 @@ impl Stackbar {
);
}
ReleaseDC(self.hwnd(), hdc);
ReleaseDC(Option::from(self.hwnd()), hdc);
// TODO: error handling
let _ = DeleteObject(hpen);
let _ = DeleteObject(hpen.into());
// TODO: error handling
let _ = DeleteObject(hbrush);
let _ = DeleteObject(hbrush.into());
// TODO: error handling
let _ = DeleteObject(hfont);
let _ = DeleteObject(hfont.into());
}
Ok(())

View File

@@ -16,6 +16,7 @@ use crate::core::BorderImplementation;
use crate::core::StackbarLabel;
use crate::core::StackbarMode;
use crate::current_virtual_desktop;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
use crate::ring::Ring;
@@ -101,21 +102,26 @@ use std::sync::Arc;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct BorderColours {
/// Border colour when the container contains a single window
#[serde(skip_serializing_if = "Option::is_none")]
pub single: Option<Colour>,
/// Border colour when the container contains multiple windows
#[serde(skip_serializing_if = "Option::is_none")]
pub stack: Option<Colour>,
/// Border colour when the container is in monocle mode
#[serde(skip_serializing_if = "Option::is_none")]
pub monocle: Option<Colour>,
/// Border colour when the container is in floating mode
#[serde(skip_serializing_if = "Option::is_none")]
pub floating: Option<Colour>,
/// Border colour when the container is unfocused
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused: Option<Colour>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct WorkspaceConfig {
/// Name
pub name: String,
@@ -125,7 +131,7 @@ pub struct WorkspaceConfig {
/// END OF LIFE FEATURE: Custom Layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_layout: Option<PathBuf>,
/// Layout rules (default: None)
/// Layout rules in the format of threshold => layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
/// END OF LIFE FEATURE: Custom layout rules (default: None)
@@ -149,8 +155,10 @@ pub struct WorkspaceConfig {
/// Determine what happens when a new window is opened (default: Create)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_container_behaviour: Option<WindowContainerBehaviour>,
/// Enable or disable float override, which makes it so every new window opens in floating mode
/// (default: false)
/// Window container behaviour rules in the format of threshold => behaviour (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_container_behaviour_rules: Option<HashMap<usize, WindowContainerBehaviour>>,
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_override: Option<bool>,
/// Specify an axis on which to flip the selected layout (default: None)
@@ -169,6 +177,12 @@ impl From<&Workspace> for WorkspaceConfig {
Layout::Custom(_) => {}
}
}
let layout_rules = (!layout_rules.is_empty()).then_some(layout_rules);
let mut window_container_behaviour_rules = HashMap::new();
for (threshold, behaviour) in value.window_container_behaviour_rules().iter().flatten() {
window_container_behaviour_rules.insert(*threshold, *behaviour);
}
let default_container_padding = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
let default_workspace_padding = DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst);
@@ -194,28 +208,42 @@ impl From<&Workspace> for WorkspaceConfig {
.name()
.clone()
.unwrap_or_else(|| String::from("unnamed")),
layout: match value.layout() {
Layout::Default(layout) => Option::from(*layout),
// TODO: figure out how we might resolve file references in the future
Layout::Custom(_) => None,
},
custom_layout: None,
layout_rules: Option::from(layout_rules),
// TODO: figure out how we might resolve file references in the future
custom_layout_rules: None,
layout: value
.tile()
.then_some(match value.layout() {
Layout::Default(layout) => Option::from(*layout),
Layout::Custom(_) => None,
})
.flatten(),
custom_layout: value
.workspace_config()
.as_ref()
.and_then(|c| c.custom_layout.clone()),
layout_rules,
custom_layout_rules: value
.workspace_config()
.as_ref()
.and_then(|c| c.custom_layout_rules.clone()),
container_padding,
workspace_padding,
initial_workspace_rules: None,
workspace_rules: None,
initial_workspace_rules: value
.workspace_config()
.as_ref()
.and_then(|c| c.initial_workspace_rules.clone()),
workspace_rules: value
.workspace_config()
.as_ref()
.and_then(|c| c.workspace_rules.clone()),
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
window_container_behaviour: *value.window_container_behaviour(),
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
float_override: *value.float_override(),
layout_flip: value.layout_flip(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct MonitorConfig {
/// Workspace configurations
pub workspaces: Vec<WorkspaceConfig>,
@@ -246,8 +274,8 @@ impl From<&Monitor> for MonitorConfig {
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.34`
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
/// The `komorebi.json` static configuration file reference for `v0.1.35`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -383,8 +411,8 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub slow_application_compensation_time: Option<u64>,
/// Komorebi status bar configuration files for multiple instances on different monitors
#[serde(skip_serializing_if = "Option::is_none")]
// this option is a little special because it is only consumed by komorebic
#[serde(skip_serializing_if = "Option::is_none")]
pub bar_configurations: Option<Vec<PathBuf>>,
/// HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars
#[serde(skip_serializing_if = "Option::is_none")]
@@ -394,18 +422,22 @@ pub struct StaticConfig {
pub floating_window_aspect_ratio: Option<AspectRatio>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct AnimationsConfig {
/// Enable or disable animations (default: false)
enabled: PerAnimationPrefixConfig<bool>,
pub enabled: PerAnimationPrefixConfig<bool>,
/// Set the animation duration in ms (default: 250)
duration: Option<PerAnimationPrefixConfig<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<PerAnimationPrefixConfig<u64>>,
/// Set the animation style (default: Linear)
style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
/// Set the animation FPS (default: 60)
fps: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fps: Option<u64>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "palette")]
pub enum KomorebiTheme {
/// A theme from catppuccin-egui
@@ -413,45 +445,63 @@ pub enum KomorebiTheme {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin,
/// Border colour when the container contains a single window (default: Blue)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container contains multiple windows (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is in monocle mode (default: Pink)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the window is floating (default: Yellow)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused (default: Base)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar focused tab text colour (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar unfocused tab text colour (default: Text)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar tab background colour (default: Base)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::CatppuccinValue>,
/// Komorebi status bar accent (default: Blue)
#[serde(skip_serializing_if = "Option::is_none")]
bar_accent: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
name: komorebi_themes::Base16,
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
bar_accent: Option<komorebi_themes::Base16Value>,
},
}
@@ -537,31 +587,41 @@ impl StaticConfig {
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct TabsConfig {
/// Width of a stackbar tab
width: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<i32>,
/// Focused tab text colour
focused_text: Option<Colour>,
#[serde(skip_serializing_if = "Option::is_none")]
pub focused_text: Option<Colour>,
/// Unfocused tab text colour
unfocused_text: Option<Colour>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused_text: Option<Colour>,
/// Tab background colour
background: Option<Colour>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<Colour>,
/// Font family
font_family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_family: Option<String>,
/// Font size
font_size: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_size: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct StackbarConfig {
/// Stackbar height
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<i32>,
/// Stackbar label
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<StackbarLabel>,
/// Stackbar mode
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<StackbarMode>,
/// Stackbar tab configuration options
#[serde(skip_serializing_if = "Option::is_none")]
pub tabs: Option<TabsConfig>,
}
@@ -1119,6 +1179,7 @@ impl StaticConfig {
let mut wm = WindowManager {
monitors: Ring::default(),
monitor_usr_idx_map: HashMap::new(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
@@ -1147,6 +1208,7 @@ impl StaticConfig {
pending_move_op: Arc::new(None),
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
uncloack_to_ignore: 0,
known_hwnds: HashMap::new(),
};
match value.focus_follows_mouse {
@@ -1180,32 +1242,78 @@ impl StaticConfig {
let value = Self::read(path)?;
let mut wm = wm.lock();
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());
let configs_with_preference: Vec<_> =
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
let mut configs_used = Vec::new();
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_matching_rules.clear();
drop(workspace_matching_rules);
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
(monitor
.serial_number_id()
.as_ref()
.is_some_and(|sn| sn == id)
|| monitor.device_id() == id)
.then_some(*c_idx)
});
c_idx
};
let idx = preferred_config_idx.or({
// Monitor without preferred config idx.
// Get index of first config that is not a preferred config of some other monitor
// and that has not been used yet. This might return `None` as well, in that case
// this monitor won't have a config tied to it and will use the default values.
let m_config_count = value
.monitors
.as_ref()
.map(|ms| ms.len())
.unwrap_or_default();
(0..m_config_count)
.find(|i| !configs_with_preference.contains(i) && !configs_used.contains(i))
});
if let Some(monitor_config) = value
.monitors
.as_ref()
.and_then(|ms| idx.and_then(|i| ms.get(i)))
{
if let Some(used_config_idx) = idx {
configs_used.push(used_config_idx);
}
monitor.ensure_workspace_count(monitor_config.workspaces.len());
monitor.set_work_area_offset(monitor_config.work_area_offset);
monitor.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
monitor.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
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() {
if let Some(workspace_config) = monitor.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
// Check if this monitor config is the preferred config for this monitor and store
// a copy of the monitor itself on the monitor cache if it is.
if idx == preferred_config_idx {
let id = monitor
.serial_number_id()
.as_ref()
.map_or(monitor.device_id(), |sn| sn);
monitor_reconciliator::insert_in_monitor_cache(id, monitor.clone());
}
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
for (j, ws) in monitor.workspaces.iter().enumerate() {
for (j, ws) in monitor_config.workspaces.iter().enumerate() {
if let Some(rules) = &ws.workspace_rules {
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
@@ -1231,6 +1339,56 @@ impl StaticConfig {
}
}
// Check for configs that should be tied to a specific display that isn't loaded right now
// and cache a monitor with those configs with the specific `serial_number_id` so that when
// those devices are connected later we can use the correct config from the cache.
if configs_with_preference.len() > configs_used.len() {
for i in configs_with_preference
.iter()
.filter(|i| !configs_used.contains(i))
{
let id = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
display_index_preferences.get(i).cloned()
};
if let (Some(id), Some(monitor_config)) =
(id, value.monitors.as_ref().and_then(|ms| ms.get(*i)))
{
// The name, device, device_id and serial_number_id can be empty here since
// once the monitor with this preferred index actually connects the
// `load_monitor_information` function will update these fields.
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"".into(),
"".into(),
"".into(),
None,
);
m.ensure_workspace_count(monitor_config.workspaces.len());
m.set_work_area_offset(monitor_config.work_area_offset);
m.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
monitor_reconciliator::insert_in_monitor_cache(&id, m);
}
}
}
wm.enforce_workspace_rules()?;
if value.border == Some(true) {
@@ -1245,29 +1403,80 @@ impl StaticConfig {
value.apply_globals()?;
if let Some(monitors) = value.monitors {
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_matching_rules.clear();
let configs_with_preference: Vec<_> =
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
let mut configs_used = Vec::new();
for (i, monitor) in monitors.iter().enumerate() {
if let Some(m) = wm.monitors_mut().get_mut(i) {
m.ensure_workspace_count(monitor.workspaces.len());
if m.work_area_offset().is_none() {
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),
);
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_matching_rules.clear();
drop(workspace_matching_rules);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
(monitor
.serial_number_id()
.as_ref()
.is_some_and(|sn| sn == id)
|| monitor.device_id() == id)
.then_some(*c_idx)
});
c_idx
};
let idx = preferred_config_idx.or({
// Monitor without preferred config idx.
// Get index of first config that is not a preferred config of some other monitor
// and that has not been used yet. This might return `None` as well, in that case
// this monitor won't have a config tied to it and will use the default values.
let m_config_count = value
.monitors
.as_ref()
.map(|ms| ms.len())
.unwrap_or_default();
(0..m_config_count)
.find(|i| !configs_with_preference.contains(i) && !configs_used.contains(i))
});
if let Some(monitor_config) = value
.monitors
.as_ref()
.and_then(|ms| idx.and_then(|i| ms.get(i)))
{
if let Some(used_config_idx) = idx {
configs_used.push(used_config_idx);
}
monitor.ensure_workspace_count(monitor_config.workspaces.len());
if monitor.work_area_offset().is_none() {
monitor.set_work_area_offset(monitor_config.work_area_offset);
}
monitor.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
monitor.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
for (j, ws) in monitor.workspaces.iter().enumerate() {
// Check if this monitor config is the preferred config for this monitor and store
// a copy of the monitor itself on the monitor cache if it is.
if idx == preferred_config_idx {
let id = monitor
.serial_number_id()
.as_ref()
.map_or(monitor.device_id(), |sn| sn);
monitor_reconciliator::insert_in_monitor_cache(id, monitor.clone());
}
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
for (j, ws) in monitor_config.workspaces.iter().enumerate() {
if let Some(rules) = &ws.workspace_rules {
for r in rules {
workspace_matching_rules.push(WorkspaceMatchingRule {
@@ -1293,6 +1502,56 @@ impl StaticConfig {
}
}
// Check for configs that should be tied to a specific display that isn't loaded right now
// and cache a monitor with those configs with the specific `serial_number_id` so that when
// those devices are connected later we can use the correct config from the cache.
if configs_with_preference.len() > configs_used.len() {
for i in configs_with_preference
.iter()
.filter(|i| !configs_used.contains(i))
{
let id = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
display_index_preferences.get(i).cloned()
};
if let (Some(id), Some(monitor_config)) =
(id, value.monitors.as_ref().and_then(|ms| ms.get(*i)))
{
// The name, device, device_id and serial_number_id can be empty here since
// once the monitor with this preferred index actually connects the
// `load_monitor_information` function will update these fields.
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"".into(),
"".into(),
"".into(),
None,
);
m.ensure_workspace_count(monitor_config.workspaces.len());
m.set_work_area_offset(monitor_config.work_area_offset);
m.set_window_based_work_area_offset(
monitor_config.window_based_work_area_offset,
);
m.set_window_based_work_area_offset_limit(
monitor_config
.window_based_work_area_offset_limit
.unwrap_or(1),
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
monitor_reconciliator::insert_in_monitor_cache(&id, m);
}
}
}
wm.enforce_workspace_rules()?;
if let Some(enabled) = value.border {

View File

@@ -40,6 +40,8 @@ use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use strum::Display;
use strum::EnumString;
use windows::Win32::Foundation::HWND;
use crate::core::ApplicationIdentifier;
@@ -297,7 +299,9 @@ impl RenderDispatcher for TransparencyRenderDispatcher {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(
Copy, Clone, Debug, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
)]
#[serde(untagged)]
pub enum AspectRatio {
/// A predefined aspect ratio
@@ -306,13 +310,22 @@ pub enum AspectRatio {
Custom(i32, i32),
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
impl Default for AspectRatio {
fn default() -> Self {
AspectRatio::Predefined(PredefinedAspectRatio::default())
}
}
#[derive(
Copy, Clone, Debug, Default, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
)]
pub enum PredefinedAspectRatio {
/// 21:9
Ultrawide,
/// 16:9
Widescreen,
/// 4:3
#[default]
Standard,
}

View File

@@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::env::temp_dir;
use std::fs::OpenOptions;
use std::io::ErrorKind;
use std::net::Shutdown;
use std::num::NonZeroUsize;
@@ -48,6 +49,8 @@ use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use crate::border_manager;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::STYLE;
use crate::config_generation::WorkspaceMatchingRule;
use crate::container::Container;
@@ -74,6 +77,7 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::Workspace;
use crate::workspace::WorkspaceLayer;
use crate::BorderColours;
use crate::Colour;
use crate::CrossBoundaryBehaviour;
@@ -99,6 +103,7 @@ use crate::WORKSPACE_MATCHING_RULES;
#[derive(Debug)]
pub struct WindowManager {
pub monitors: Ring<Monitor>,
pub monitor_usr_idx_map: HashMap<usize, usize>,
pub incoming_events: Receiver<WindowManagerEvent>,
pub command_listener: UnixListener,
pub is_paused: bool,
@@ -116,12 +121,15 @@ pub struct WindowManager {
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
pub uncloack_to_ignore: usize,
/// Maps each known window hwnd to the (monitor, workspace) index pair managing it
pub known_hwnds: HashMap<isize, (usize, usize)>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct State {
pub monitors: Ring<Monitor>,
pub monitor_usr_idx_map: HashMap<usize, usize>,
pub is_paused: bool,
pub resize_delta: i32,
pub new_window_behaviour: WindowContainerBehaviour,
@@ -281,8 +289,69 @@ impl AsRef<Self> for WindowManager {
impl From<&WindowManager> for State {
fn from(wm: &WindowManager) -> Self {
// This is used to remove any information that doesn't need to be passed on to subscribers
// or to be shown with the `komorebic state` command. Currently it is only removing the
// `workspace_config` field from every workspace, but more stripping can be added later if
// needed.
let mut stripped_monitors = Ring::default();
*stripped_monitors.elements_mut() = wm
.monitors()
.iter()
.map(|monitor| Monitor {
id: monitor.id,
name: monitor.name.clone(),
device: monitor.device.clone(),
device_id: monitor.device_id.clone(),
serial_number_id: monitor.serial_number_id.clone(),
size: monitor.size,
work_area_size: monitor.work_area_size,
work_area_offset: monitor.work_area_offset,
window_based_work_area_offset: monitor.window_based_work_area_offset,
window_based_work_area_offset_limit: monitor.window_based_work_area_offset_limit,
workspaces: {
let mut ws = Ring::default();
*ws.elements_mut() = monitor
.workspaces()
.iter()
.map(|workspace| Workspace {
name: workspace.name.clone(),
containers: workspace.containers.clone(),
monocle_container: workspace.monocle_container.clone(),
monocle_container_restore_idx: workspace.monocle_container_restore_idx,
maximized_window: workspace.maximized_window,
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
floating_windows: workspace.floating_windows.clone(),
layout: workspace.layout.clone(),
layout_rules: workspace.layout_rules.clone(),
layout_flip: workspace.layout_flip,
workspace_padding: workspace.workspace_padding,
container_padding: workspace.container_padding,
latest_layout: workspace.latest_layout.clone(),
resize_dimensions: workspace.resize_dimensions.clone(),
tile: workspace.tile,
apply_window_based_work_area_offset: workspace
.apply_window_based_work_area_offset,
window_container_behaviour: workspace.window_container_behaviour,
window_container_behaviour_rules: workspace
.window_container_behaviour_rules
.clone(),
float_override: workspace.float_override,
layer: workspace.layer,
workspace_config: None,
})
.collect::<VecDeque<_>>();
ws.focus(monitor.workspaces.focused_idx());
ws
},
last_focused_workspace: monitor.last_focused_workspace,
workspace_names: monitor.workspace_names.clone(),
})
.collect::<VecDeque<_>>();
stripped_monitors.focus(wm.monitors.focused_idx());
Self {
monitors: wm.monitors.clone(),
monitors: stripped_monitors,
monitor_usr_idx_map: wm.monitor_usr_idx_map.clone(),
is_paused: wm.is_paused,
work_area_offset: wm.work_area_offset,
resize_delta: wm.resize_delta,
@@ -343,6 +412,7 @@ impl WindowManager {
Ok(Self {
monitors: Ring::default(),
monitor_usr_idx_map: HashMap::new(),
incoming_events: incoming,
command_listener: listener,
is_paused: false,
@@ -360,13 +430,14 @@ impl WindowManager {
pending_move_op: Arc::new(None),
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
uncloack_to_ignore: 0,
known_hwnds: HashMap::new(),
})
}
#[tracing::instrument(skip(self))]
pub fn init(&mut self) -> Result<()> {
tracing::info!("initialising");
WindowsApi::load_monitor_information(&mut self.monitors)?;
WindowsApi::load_monitor_information(self)?;
WindowsApi::load_workspace_information(&mut self.monitors)
}
@@ -1325,86 +1396,168 @@ impl WindowManager {
delta: i32,
update: bool,
) -> Result<()> {
let work_area = self.focused_monitor_work_area()?;
let mouse_follows_focus = self.mouse_follows_focus;
let mut focused_monitor_work_area = self.focused_monitor_work_area()?;
let workspace = self.focused_workspace_mut()?;
match workspace.layout() {
Layout::Default(layout) => {
tracing::info!("resizing window");
let len = NonZeroUsize::new(workspace.containers().len())
.ok_or_else(|| anyhow!("there must be at least one container"))?;
let focused_idx = workspace.focused_container_idx();
let focused_idx_resize = workspace
.resize_dimensions()
.get(focused_idx)
.ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?;
match workspace.layer() {
WorkspaceLayer::Floating => {
let workspace = self.focused_workspace()?;
let focused_hwnd = WindowsApi::foreground_window()?;
if direction
.destination(
workspace.layout().as_boxed_direction().as_ref(),
workspace.layout_flip(),
focused_idx,
len,
)
.is_some()
{
let unaltered = layout.calculate(
&work_area,
len,
workspace.container_padding(),
workspace.layout_flip(),
&[],
);
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
focused_monitor_work_area.left += border_offset;
focused_monitor_work_area.left += border_width;
focused_monitor_work_area.top += border_offset;
focused_monitor_work_area.top += border_width;
focused_monitor_work_area.right -= border_offset;
focused_monitor_work_area.right -= border_width;
focused_monitor_work_area.bottom -= border_offset;
focused_monitor_work_area.bottom -= border_width;
let mut direction = direction;
// We only ever want to operate on the unflipped Rect positions when resizing, then we
// can flip them however they need to be flipped once the resizing has been done
if let Some(flip) = workspace.layout_flip() {
match flip {
Axis::Horizontal => {
if matches!(direction, OperationDirection::Left)
|| matches!(direction, OperationDirection::Right)
{
direction = direction.opposite();
for window in workspace.floating_windows().iter() {
if window.hwnd == focused_hwnd {
let mut rect = WindowsApi::window_rect(window.hwnd)?;
match (direction, sizing) {
(OperationDirection::Left, Sizing::Increase) => {
if rect.left - delta < focused_monitor_work_area.left {
rect.left = focused_monitor_work_area.left;
} else {
rect.left -= delta;
}
}
Axis::Vertical => {
if matches!(direction, OperationDirection::Up)
|| matches!(direction, OperationDirection::Down)
(OperationDirection::Left, Sizing::Decrease) => {
rect.left += delta;
}
(OperationDirection::Right, Sizing::Increase) => {
if rect.left + rect.right + delta * 2
> focused_monitor_work_area.right
{
direction = direction.opposite();
rect.right = focused_monitor_work_area.right - rect.left;
} else {
rect.right += delta * 2;
}
}
Axis::HorizontalAndVertical => direction = direction.opposite(),
(OperationDirection::Right, Sizing::Decrease) => {
rect.right -= delta * 2;
}
(OperationDirection::Up, Sizing::Increase) => {
if rect.top - delta < focused_monitor_work_area.top {
rect.top = focused_monitor_work_area.top;
} else {
rect.top -= delta;
}
}
(OperationDirection::Up, Sizing::Decrease) => {
rect.top += delta;
}
(OperationDirection::Down, Sizing::Increase) => {
if rect.top + rect.bottom + delta * 2
> focused_monitor_work_area.bottom
{
rect.bottom = focused_monitor_work_area.bottom - rect.top;
} else {
rect.bottom += delta * 2;
}
}
(OperationDirection::Down, Sizing::Decrease) => {
rect.bottom -= delta * 2;
}
}
WindowsApi::position_window(window.hwnd, &rect, false)?;
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&rect)?;
}
break;
}
let resize = layout.resize(
unaltered
.get(focused_idx)
.ok_or_else(|| anyhow!("there is no last layout"))?,
focused_idx_resize,
direction,
sizing,
delta,
);
workspace.resize_dimensions_mut()[focused_idx] = resize;
return if update {
self.update_focused_workspace(false, false)
} else {
Ok(())
};
}
tracing::warn!("cannot resize container in this direction");
}
Layout::Custom(_) => {
tracing::warn!("containers cannot be resized when using custom layouts");
WorkspaceLayer::Tiling => {
match workspace.layout() {
Layout::Default(layout) => {
tracing::info!("resizing window");
let len = NonZeroUsize::new(workspace.containers().len())
.ok_or_else(|| anyhow!("there must be at least one container"))?;
let focused_idx = workspace.focused_container_idx();
let focused_idx_resize = workspace
.resize_dimensions()
.get(focused_idx)
.ok_or_else(|| {
anyhow!("there is no resize adjustment for this container")
})?;
if direction
.destination(
workspace.layout().as_boxed_direction().as_ref(),
workspace.layout_flip(),
focused_idx,
len,
)
.is_some()
{
let unaltered = layout.calculate(
&focused_monitor_work_area,
len,
workspace.container_padding(),
workspace.layout_flip(),
&[],
);
let mut direction = direction;
// We only ever want to operate on the unflipped Rect positions when resizing, then we
// can flip them however they need to be flipped once the resizing has been done
if let Some(flip) = workspace.layout_flip() {
match flip {
Axis::Horizontal => {
if matches!(direction, OperationDirection::Left)
|| matches!(direction, OperationDirection::Right)
{
direction = direction.opposite();
}
}
Axis::Vertical => {
if matches!(direction, OperationDirection::Up)
|| matches!(direction, OperationDirection::Down)
{
direction = direction.opposite();
}
}
Axis::HorizontalAndVertical => direction = direction.opposite(),
}
}
let resize = layout.resize(
unaltered
.get(focused_idx)
.ok_or_else(|| anyhow!("there is no last layout"))?,
focused_idx_resize,
direction,
sizing,
delta,
);
workspace.resize_dimensions_mut()[focused_idx] = resize;
return if update {
self.update_focused_workspace(false, false)
} else {
Ok(())
};
}
tracing::warn!("cannot resize container in this direction");
}
Layout::Custom(_) => {
tracing::warn!("containers cannot be resized when using custom layouts");
}
}
}
}
Ok(())
}
@@ -1793,6 +1946,56 @@ impl WindowManager {
self.update_focused_workspace(mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn focus_floating_window_in_direction(
&mut self,
direction: OperationDirection,
) -> Result<()> {
let mouse_follows_focus = self.mouse_follows_focus;
let focused_workspace = self.focused_workspace()?;
let mut target_idx = None;
let len = focused_workspace.floating_windows().len();
if len > 1 {
let focused_hwnd = WindowsApi::foreground_window()?;
for (idx, window) in focused_workspace.floating_windows().iter().enumerate() {
if window.hwnd == focused_hwnd {
match direction {
OperationDirection::Left => {}
OperationDirection::Right => {}
OperationDirection::Up => {
if idx == len - 1 {
target_idx = Some(0)
} else {
target_idx = Some(idx + 1)
}
}
OperationDirection::Down => {
if idx == 0 {
target_idx = Some(len - 1)
} else {
target_idx = Some(idx - 1)
}
}
}
}
}
if target_idx.is_none() {
target_idx = Some(0);
}
}
if let Some(idx) = target_idx {
if let Some(window) = focused_workspace.floating_windows().get(idx) {
window.focus(mouse_follows_focus)?;
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn focus_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -1938,6 +2141,75 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_floating_window_in_direction(
&mut self,
direction: OperationDirection,
) -> Result<()> {
let mouse_follows_focus = self.mouse_follows_focus;
let mut focused_monitor_work_area = self.focused_monitor_work_area()?;
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
focused_monitor_work_area.left += border_offset;
focused_monitor_work_area.left += border_width;
focused_monitor_work_area.top += border_offset;
focused_monitor_work_area.top += border_width;
focused_monitor_work_area.right -= border_offset;
focused_monitor_work_area.right -= border_width;
focused_monitor_work_area.bottom -= border_offset;
focused_monitor_work_area.bottom -= border_width;
let focused_workspace = self.focused_workspace()?;
let delta = self.resize_delta;
let focused_hwnd = WindowsApi::foreground_window()?;
for window in focused_workspace.floating_windows().iter() {
if window.hwnd == focused_hwnd {
let mut rect = WindowsApi::window_rect(window.hwnd)?;
match direction {
OperationDirection::Left => {
if rect.left - delta < focused_monitor_work_area.left {
rect.left = focused_monitor_work_area.left;
} else {
rect.left -= delta;
}
}
OperationDirection::Right => {
if rect.left + delta + rect.right > focused_monitor_work_area.right {
rect.left = focused_monitor_work_area.right - rect.right;
} else {
rect.left += delta;
}
}
OperationDirection::Up => {
if rect.top - delta < focused_monitor_work_area.top {
rect.top = focused_monitor_work_area.top;
} else {
rect.top -= delta;
}
}
OperationDirection::Down => {
if rect.top + delta + rect.bottom > focused_monitor_work_area.bottom {
rect.top = focused_monitor_work_area.bottom - rect.bottom;
} else {
rect.top += delta;
}
}
}
WindowsApi::position_window(window.hwnd, &rect, false)?;
if mouse_follows_focus {
WindowsApi::center_cursor_in_rect(&rect)?;
}
break;
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn move_container_in_direction(&mut self, direction: OperationDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -2114,6 +2386,54 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn focus_floating_window_in_cycle_direction(
&mut self,
direction: CycleDirection,
) -> Result<()> {
let mouse_follows_focus = self.mouse_follows_focus;
let focused_workspace = self.focused_workspace()?;
let mut target_idx = None;
let len = focused_workspace.floating_windows().len();
if len > 1 {
let focused_hwnd = WindowsApi::foreground_window()?;
for (idx, window) in focused_workspace.floating_windows().iter().enumerate() {
if window.hwnd == focused_hwnd {
match direction {
CycleDirection::Previous => {
if idx == 0 {
target_idx = Some(len - 1)
} else {
target_idx = Some(idx - 1)
}
}
CycleDirection::Next => {
if idx == len - 1 {
target_idx = Some(0)
} else {
target_idx = Some(idx - 1)
}
}
}
}
}
if target_idx.is_none() {
target_idx = Some(0);
}
}
if let Some(idx) = target_idx {
if let Some(window) = focused_workspace.floating_windows().get(idx) {
window.focus(mouse_follows_focus)?;
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn focus_container_in_cycle_direction(&mut self, direction: CycleDirection) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -3296,4 +3616,66 @@ impl WindowManager {
.focused_window_mut()
.ok_or_else(|| anyhow!("there is no window"))
}
/// Updates the list of `known_hwnds` and their monitor/workspace index pair
///
/// [`known_hwnds`]: `Self.known_hwnds`
pub fn update_known_hwnds(&mut self) {
tracing::trace!("updating list of known hwnds");
let mut known_hwnds = HashMap::new();
for (m_idx, monitor) in self.monitors().iter().enumerate() {
for (w_idx, workspace) in monitor.workspaces().iter().enumerate() {
for container in workspace.containers() {
for window in container.windows() {
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
}
}
for window in workspace.floating_windows() {
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
}
if let Some(window) = workspace.maximized_window() {
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
}
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
}
}
}
}
if self.known_hwnds != known_hwnds {
// Update reaper cache
{
let mut reaper_cache = crate::reaper::HWNDS_CACHE.lock();
*reaper_cache = known_hwnds.clone();
}
// Save to file
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
match OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(hwnd_json)
{
Ok(file) => {
if let Err(error) =
serde_json::to_writer_pretty(&file, &known_hwnds.keys().collect::<Vec<_>>())
{
tracing::error!("Failed to save list of known_hwnds on file: {}", error);
}
}
Err(error) => {
tracing::error!("Failed to save list of known_hwnds on file: {}", error);
}
}
// Store new hwnds
self.known_hwnds = known_hwnds;
}
}
}

View File

@@ -1,6 +1,7 @@
use core::ffi::c_void;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::ffi::c_void;
use std::mem::size_of;
use color_eyre::eyre::anyhow;
@@ -11,7 +12,6 @@ use windows::core::Result as WindowsCrateResult;
use windows::core::PCWSTR;
use windows::core::PWSTR;
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Foundation::HINSTANCE;
@@ -48,6 +48,8 @@ use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Power::RegisterPowerSettingNotification;
use windows::Win32::System::Power::HPOWERNOTIFY;
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
use windows::Win32::System::RemoteDesktop::WTSRegisterSessionNotification;
use windows::Win32::System::Threading::GetCurrentProcessId;
@@ -92,6 +94,7 @@ use windows::Win32::UI::WindowsAndMessaging::MoveWindow;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
use windows::Win32::UI::WindowsAndMessaging::RegisterDeviceNotificationW;
use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
@@ -101,11 +104,14 @@ 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::DEV_BROADCAST_DEVICEINTERFACE_W;
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::HDEVNOTIFY;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
@@ -133,6 +139,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use windows_core::BOOL;
use crate::core::Rect;
@@ -143,6 +150,7 @@ use crate::ring::Ring;
use crate::set_window_position::SetWindowPosition;
use crate::windows_callbacks;
use crate::Window;
use crate::WindowManager;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::MONITOR_INDEX_PREFERENCES;
@@ -228,16 +236,9 @@ impl WindowsApi {
callback: MONITORENUMPROC,
callback_data_address: isize,
) -> Result<()> {
unsafe {
EnumDisplayMonitors(
HDC(std::ptr::null_mut()),
None,
callback,
LPARAM(callback_data_address),
)
}
.ok()
.process()
unsafe { EnumDisplayMonitors(None, None, callback, LPARAM(callback_data_address)) }
.ok()
.process()
}
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
@@ -252,7 +253,10 @@ impl WindowsApi {
.collect::<Vec<_>>())
}
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
pub fn load_monitor_information(wm: &mut WindowManager) -> Result<()> {
let monitors = &mut wm.monitors;
let monitor_usr_idx_map = &mut wm.monitor_usr_idx_map;
'read: for display in win32_display_data::connected_displays_all().flatten() {
let path = display.device_path.clone();
@@ -296,7 +300,8 @@ impl WindowsApi {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
for (index, id) in &*display_index_preferences {
if id.eq(m.device_id()) {
if m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || id.eq(m.device_id())
{
index_preference = Option::from(index);
}
}
@@ -325,6 +330,40 @@ impl WindowsApi {
.elements_mut()
.retain(|m| m.name().ne("PLACEHOLDER"));
// Rebuild monitor index map
*monitor_usr_idx_map = HashMap::new();
let mut added_monitor_idxs = Vec::new();
for (index, id) in &*DISPLAY_INDEX_PREFERENCES.lock() {
if let Some(m_idx) = monitors.elements().iter().position(|m| {
m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || m.device_id() == id
}) {
monitor_usr_idx_map.insert(*index, m_idx);
added_monitor_idxs.push(m_idx);
}
}
let max_usr_idx = monitors
.elements()
.len()
.max(monitor_usr_idx_map.keys().max().map_or(0, |v| *v));
let mut available_usr_idxs = (0..max_usr_idx)
.filter(|i| !monitor_usr_idx_map.contains_key(i))
.collect::<Vec<_>>();
let not_added_monitor_idxs = (0..monitors.elements().len())
.filter(|i| !added_monitor_idxs.contains(i))
.collect::<Vec<_>>();
for i in not_added_monitor_idxs {
if let Some(next_usr_idx) = available_usr_idxs.first() {
monitor_usr_idx_map.insert(*next_usr_idx, i);
available_usr_idxs.remove(0);
} else if let Some(idx) = monitor_usr_idx_map.keys().max() {
monitor_usr_idx_map.insert(*idx, i);
}
}
Ok(())
}
@@ -473,7 +512,7 @@ impl WindowsApi {
unsafe {
SetWindowPos(
hwnd,
position,
Option::from(position),
layout.left,
layout.top,
layout.right,
@@ -511,7 +550,7 @@ impl WindowsApi {
}
fn post_message(hwnd: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> Result<()> {
unsafe { PostMessageW(hwnd, message, wparam, lparam) }.process()
unsafe { PostMessageW(Option::from(hwnd), message, wparam, lparam) }.process()
}
pub fn close_window(hwnd: isize) -> Result<()> {
@@ -554,7 +593,7 @@ impl WindowsApi {
// Error ignored, as the operation is not always necessary.
let _ = SetWindowPos(
HWND(as_ptr!(hwnd)),
HWND_TOP,
Option::from(HWND_TOP),
0,
0,
0,
@@ -570,7 +609,7 @@ impl WindowsApi {
#[allow(dead_code)]
pub fn top_window() -> Result<isize> {
unsafe { GetTopWindow(HWND::default())? }.process()
unsafe { GetTopWindow(None)? }.process()
}
pub fn desktop_window() -> Result<isize> {
@@ -886,7 +925,7 @@ impl WindowsApi {
}
pub fn is_window(hwnd: isize) -> bool {
unsafe { IsWindow(HWND(as_ptr!(hwnd))) }.into()
unsafe { IsWindow(Option::from(HWND(as_ptr!(hwnd)))) }.into()
}
pub fn is_window_visible(hwnd: isize) -> bool {
@@ -1114,7 +1153,7 @@ impl WindowsApi {
CW_USEDEFAULT,
None,
None,
HINSTANCE(as_ptr!(instance)),
Option::from(HINSTANCE(as_ptr!(instance))),
Some(border as _),
)?
}
@@ -1163,16 +1202,35 @@ impl WindowsApi {
CW_USEDEFAULT,
None,
None,
HINSTANCE(as_ptr!(instance)),
Option::from(HINSTANCE(as_ptr!(instance))),
None,
)?
}
.process()
}
pub fn register_power_setting_notification(
hwnd: isize,
guid: &windows_core::GUID,
flags: REGISTER_NOTIFICATION_FLAGS,
) -> WindowsCrateResult<HPOWERNOTIFY> {
unsafe { RegisterPowerSettingNotification(HANDLE::from(HWND(as_ptr!(hwnd))), guid, flags) }
}
pub fn register_device_notification(
hwnd: isize,
mut filter: DEV_BROADCAST_DEVICEINTERFACE_W,
flags: REGISTER_NOTIFICATION_FLAGS,
) -> WindowsCrateResult<HDEVNOTIFY> {
unsafe {
let state_ptr: *const c_void = &mut filter as *mut _ as *const c_void;
RegisterDeviceNotificationW(HANDLE::from(HWND(as_ptr!(hwnd))), state_ptr, flags)
}
}
pub fn invalidate_rect(hwnd: isize, rect: Option<&Rect>, erase: bool) -> bool {
let rect = rect.map(|rect| &rect.rect() as *const RECT);
unsafe { InvalidateRect(HWND(as_ptr!(hwnd)), rect, erase) }.as_bool()
unsafe { InvalidateRect(Option::from(HWND(as_ptr!(hwnd))), rect, erase) }.as_bool()
}
pub fn alt_is_pressed() -> bool {

View File

@@ -8,7 +8,6 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::WPARAM;
@@ -21,6 +20,7 @@ use windows::Win32::UI::WindowsAndMessaging::OBJID_WINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows_core::BOOL;
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
@@ -115,6 +115,16 @@ pub extern "system" fn win_event_hook(
}
}
// sometimes the border focus state and colors don't get updated because this event comes too
// slow for the value of GetForegroundWindow to be up to date by the time it is inspected in
// the border manager to determine if a window show have its border show as "focused"
//
// so here we can just fire another event at the border manager when the system has finally
// registered the new foreground window and this time the correct border colors will be applied
if matches!(winevent, WinEvent::SystemForeground) && !has_filtered_style(hwnd) {
border_manager::send_notification(Some(hwnd.0 as isize));
}
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => {
tracing::trace!(

View File

@@ -3,7 +3,6 @@ use std::time::Duration;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Accessibility::SetWinEventHook;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
@@ -41,7 +40,7 @@ pub fn start() {
loop {
unsafe {
if !GetMessageW(&mut msg, HWND(std::ptr::null_mut()), 0, 0).as_bool() {
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
tracing::debug!("windows event processing thread shutdown");
break;
};

View File

@@ -1,4 +1,6 @@
use std::collections::VecDeque;
use std::fmt::Display;
use std::fmt::Formatter;
use std::num::NonZeroUsize;
use std::sync::atomic::Ordering;
@@ -54,42 +56,65 @@ use crate::REMOVE_TITLEBARS;
)]
pub struct Workspace {
#[getset(get = "pub", set = "pub")]
name: Option<String>,
containers: Ring<Container>,
pub name: Option<String>,
pub containers: Ring<Container>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
monocle_container: Option<Container>,
pub monocle_container: Option<Container>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
monocle_container_restore_idx: Option<usize>,
pub monocle_container_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
maximized_window: Option<Window>,
pub maximized_window: Option<Window>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
maximized_window_restore_idx: Option<usize>,
pub maximized_window_restore_idx: Option<usize>,
#[getset(get = "pub", get_mut = "pub")]
floating_windows: Vec<Window>,
pub floating_windows: Vec<Window>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
layout: Layout,
pub layout: Layout,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
layout_rules: Vec<(usize, Layout)>,
pub layout_rules: Vec<(usize, Layout)>,
#[getset(get_copy = "pub", set = "pub")]
layout_flip: Option<Axis>,
pub layout_flip: Option<Axis>,
#[getset(get_copy = "pub", set = "pub")]
workspace_padding: Option<i32>,
pub workspace_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
container_padding: Option<i32>,
pub container_padding: Option<i32>,
#[getset(get = "pub", set = "pub")]
latest_layout: Vec<Rect>,
pub latest_layout: Vec<Rect>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
resize_dimensions: Vec<Option<Rect>>,
pub resize_dimensions: Vec<Option<Rect>>,
#[getset(get = "pub", set = "pub")]
tile: bool,
pub tile: bool,
#[getset(get_copy = "pub", set = "pub")]
apply_window_based_work_area_offset: bool,
pub apply_window_based_work_area_offset: bool,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
window_container_behaviour: Option<WindowContainerBehaviour>,
pub window_container_behaviour: Option<WindowContainerBehaviour>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
float_override: Option<bool>,
pub window_container_behaviour_rules: Option<Vec<(usize, WindowContainerBehaviour)>>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub float_override: Option<bool>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub layer: WorkspaceLayer,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
pub workspace_config: Option<WorkspaceConfig>,
}
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub enum WorkspaceLayer {
#[default]
Tiling,
Floating,
}
impl Display for WorkspaceLayer {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
WorkspaceLayer::Tiling => write!(f, "Tiling"),
WorkspaceLayer::Floating => write!(f, "Floating"),
}
}
}
impl_ring_elements!(Workspace, Container);
@@ -114,7 +139,10 @@ impl Default for Workspace {
tile: true,
apply_window_based_work_area_offset: true,
window_container_behaviour: None,
window_container_behaviour_rules: None,
float_override: None,
workspace_config: None,
layer: Default::default(),
}
}
}
@@ -158,42 +186,55 @@ impl Workspace {
self.tile = false;
}
let mut all_layout_rules = vec![];
if let Some(layout_rules) = &config.layout_rules {
let mut all_rules = vec![];
for (count, rule) in layout_rules {
all_rules.push((*count, Layout::Default(*rule)));
all_layout_rules.push((*count, Layout::Default(*rule)));
}
self.set_layout_rules(all_rules);
all_layout_rules.sort_by_key(|(i, _)| *i);
self.tile = true;
}
self.set_layout_rules(all_layout_rules.clone());
if let Some(layout_rules) = &config.custom_layout_rules {
let rules = self.layout_rules_mut();
for (count, pathbuf) in layout_rules {
let rule = CustomLayout::from_path(pathbuf)?;
rules.push((*count, Layout::Custom(rule)));
all_layout_rules.push((*count, Layout::Custom(rule)));
}
all_layout_rules.sort_by_key(|(i, _)| *i);
self.tile = true;
self.set_layout_rules(all_layout_rules);
}
self.set_apply_window_based_work_area_offset(
config.apply_window_based_work_area_offset.unwrap_or(true),
);
if config.window_container_behaviour.is_some() {
self.set_window_container_behaviour(config.window_container_behaviour);
self.set_window_container_behaviour(config.window_container_behaviour);
if let Some(window_container_behaviour_rules) = &config.window_container_behaviour_rules {
if window_container_behaviour_rules.is_empty() {
self.set_window_container_behaviour_rules(None);
} else {
let mut all_rules = vec![];
for (count, behaviour) in window_container_behaviour_rules {
all_rules.push((*count, *behaviour));
}
all_rules.sort_by_key(|(i, _)| *i);
self.set_window_container_behaviour_rules(Some(all_rules));
}
} else {
self.set_window_container_behaviour_rules(None);
}
if config.float_override.is_some() {
self.set_float_override(config.float_override);
}
self.set_float_override(config.float_override);
self.set_layout_flip(config.layout_flip);
if config.layout_flip.is_some() {
self.set_layout_flip(config.layout_flip);
}
self.set_workspace_config(Some(config.clone()));
Ok(())
}
@@ -326,9 +367,9 @@ impl Workspace {
if !self.layout_rules().is_empty() {
let mut updated_layout = None;
for rule in self.layout_rules() {
if self.containers().len() >= rule.0 {
updated_layout = Option::from(rule.1.clone());
for (threshold, layout) in self.layout_rules() {
if self.containers().len() >= *threshold {
updated_layout = Option::from(layout.clone());
}
}
@@ -337,6 +378,17 @@ impl Workspace {
}
}
if let Some(window_container_behaviour_rules) = self.window_container_behaviour_rules() {
let mut updated_behaviour = None;
for (threshold, behaviour) in window_container_behaviour_rules {
if self.containers().len() >= *threshold {
updated_behaviour = Option::from(*behaviour);
}
}
self.set_window_container_behaviour(updated_behaviour);
}
let managed_maximized_window = self.maximized_window().is_some();
if *self.tile() {
@@ -433,7 +485,16 @@ impl Workspace {
// number of layouts / containers. This should never actually truncate as the remove_window
// function takes care of cleaning up resize dimensions when destroying empty containers
let container_count = self.containers().len();
self.resize_dimensions_mut().resize(container_count, None);
// since monocle is a toggle, we never want to truncate the resize dimensions since it will
// almost always be toggled off and the container will be reintegrated into layout
//
// without this check, if there are exactly two containers, when one is toggled to monocle
// the resize dimensions will be truncated to len == 1, and when it is reintegrated, if it
// had a resize adjustment before, that will have been lost
if self.monocle_container().is_none() {
self.resize_dimensions_mut().resize(container_count, None);
}
Ok(())
}
@@ -465,7 +526,15 @@ impl Workspace {
}
for window in self.visible_windows().into_iter().flatten() {
if !window.is_window() {
if !window.is_window()
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
// docs is closed
//
// I hate every single person who worked on Microsoft Office 365, especially Word
|| !window.is_visible()
{
hwnds.push(window.hwnd);
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.34"
version = "0.1.35"
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.34"
version = "0.1.35"
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
@@ -24,13 +24,10 @@ reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
shadow-rs = { workspace = true }
sysinfo = { workspace = true }
thiserror = "2"
uds_windows = { workspace = true }
which = { workspace = true }
win32-display-data = { workspace = true }
windows = { workspace = true }
[build-dependencies]

View File

@@ -1108,6 +1108,8 @@ enum SubCommand {
/// Focus the specified monitor
#[clap(arg_required_else_help = true)]
FocusMonitor(FocusMonitor),
/// Focus the monitor at the current cursor location
FocusMonitorAtCursor,
/// Focus the last focused workspace on the focused monitor
FocusLastWorkspace,
/// Focus the specified workspace on the focused monitor
@@ -1153,6 +1155,8 @@ enum SubCommand {
/// Set offsets for a monitor to exclude parts of the work area from tiling
#[clap(arg_required_else_help = true)]
MonitorWorkAreaOffset(MonitorWorkAreaOffset),
/// Toggle application of the window-based work area offset for the focused workspace
ToggleWindowBasedWorkAreaOffset,
/// Set container padding on the focused workspace
#[clap(arg_required_else_help = true)]
FocusedWorkspaceContainerPadding(FocusedWorkspaceContainerPadding),
@@ -1265,6 +1269,8 @@ enum SubCommand {
/// mode, for the currently focused workspace. If there was no override value set for the
/// workspace previously it takes the opposite of the global value.
ToggleWorkspaceFloatOverride,
/// Toggle between the Tiling and Floating layers on the focused workspace
ToggleWorkspaceLayer,
/// Toggle window tiling on the focused workspace
TogglePause,
/// Toggle window tiling on the focused workspace
@@ -1876,6 +1882,9 @@ fn main() -> Result<()> {
bottom: arg.bottom,
}))?;
}
SubCommand::ToggleWindowBasedWorkAreaOffset => {
send_message(&SocketMessage::ToggleWindowBasedWorkAreaOffset)?;
}
SubCommand::ContainerPadding(arg) => {
send_message(&SocketMessage::ContainerPadding(
arg.monitor,
@@ -2588,6 +2597,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::FocusMonitor(arg) => {
send_message(&SocketMessage::FocusMonitorNumber(arg.target))?;
}
SubCommand::FocusMonitorAtCursor => {
send_message(&SocketMessage::FocusMonitorAtCursor)?;
}
SubCommand::FocusLastWorkspace => {
send_message(&SocketMessage::FocusLastWorkspace)?;
}
@@ -2844,6 +2856,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::ToggleWorkspaceFloatOverride => {
send_message(&SocketMessage::ToggleWorkspaceFloatOverride)?;
}
SubCommand::ToggleWorkspaceLayer => {
send_message(&SocketMessage::ToggleWorkspaceLayer)?;
}
SubCommand::WindowHidingBehaviour(arg) => {
send_message(&SocketMessage::WindowHidingBehaviour(arg.hiding_behaviour))?;
}

View File

@@ -127,6 +127,7 @@ nav:
- cli/send-to-monitor-workspace.md
- cli/move-to-monitor-workspace.md
- cli/focus-monitor.md
- cli/focus-monitor-at-cursor.md
- cli/focus-last-workspace.md
- cli/focus-workspace.md
- cli/focus-workspaces.md
@@ -143,6 +144,7 @@ nav:
- cli/invisible-borders.md
- cli/global-work-area-offset.md
- cli/monitor-work-area-offset.md
- cli/toggle-window-based-work-area-offset.md
- cli/focused-workspace-container-padding.md
- cli/focused-workspace-padding.md
- cli/adjust-container-padding.md
@@ -175,6 +177,7 @@ nav:
- cli/toggle-float-override.md
- cli/toggle-workspace-window-container-behaviour.md
- cli/toggle-workspace-float-override.md
- cli/toggle-workspace-layer.md
- cli/toggle-pause.md
- cli/toggle-tiling.md
- cli/toggle-float.md

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "KomobarConfig",
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.34`",
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.35`",
"type": "object",
"required": [
"left_widgets",
@@ -271,6 +271,66 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Keyboard"
],
"properties": {
"Keyboard": {
"type": "object",
"required": [
"enable"
],
"properties": {
"data_refresh_interval": {
"description": "Data refresh interval (default: 1 second)",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"enable": {
"description": "Enable the Input widget",
"type": "boolean"
},
"label_prefix": {
"description": "Display label prefix",
"oneOf": [
{
"description": "Show no prefix",
"type": "string",
"enum": [
"None"
]
},
{
"description": "Show an icon",
"type": "string",
"enum": [
"Icon"
]
},
{
"description": "Show text",
"type": "string",
"enum": [
"Text"
]
},
{
"description": "Show an icon and text",
"type": "string",
"enum": [
"IconAndText"
]
}
]
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -444,6 +504,19 @@
}
}
},
"workspace_layer": {
"description": "Configure the Workspace Layer widget",
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Workspace Layer widget",
"type": "boolean"
}
}
},
"workspaces": {
"description": "Configure the Workspaces widget",
"type": "object",
@@ -1518,6 +1591,66 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Keyboard"
],
"properties": {
"Keyboard": {
"type": "object",
"required": [
"enable"
],
"properties": {
"data_refresh_interval": {
"description": "Data refresh interval (default: 1 second)",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"enable": {
"description": "Enable the Input widget",
"type": "boolean"
},
"label_prefix": {
"description": "Display label prefix",
"oneOf": [
{
"description": "Show no prefix",
"type": "string",
"enum": [
"None"
]
},
{
"description": "Show an icon",
"type": "string",
"enum": [
"Icon"
]
},
{
"description": "Show text",
"type": "string",
"enum": [
"Text"
]
},
{
"description": "Show an icon and text",
"type": "string",
"enum": [
"IconAndText"
]
}
]
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -1691,6 +1824,19 @@
}
}
},
"workspace_layer": {
"description": "Configure the Workspace Layer widget",
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Workspace Layer widget",
"type": "boolean"
}
}
},
"workspaces": {
"description": "Configure the Workspaces widget",
"type": "object",
@@ -2698,6 +2844,66 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Keyboard"
],
"properties": {
"Keyboard": {
"type": "object",
"required": [
"enable"
],
"properties": {
"data_refresh_interval": {
"description": "Data refresh interval (default: 1 second)",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"enable": {
"description": "Enable the Input widget",
"type": "boolean"
},
"label_prefix": {
"description": "Display label prefix",
"oneOf": [
{
"description": "Show no prefix",
"type": "string",
"enum": [
"None"
]
},
{
"description": "Show an icon",
"type": "string",
"enum": [
"Icon"
]
},
{
"description": "Show text",
"type": "string",
"enum": [
"Text"
]
},
{
"description": "Show an icon and text",
"type": "string",
"enum": [
"IconAndText"
]
}
]
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -2871,6 +3077,19 @@
}
}
},
"workspace_layer": {
"description": "Configure the Workspace Layer widget",
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Workspace Layer widget",
"type": "boolean"
}
}
},
"workspaces": {
"description": "Configure the Workspaces widget",
"type": "object",
@@ -3423,7 +3642,7 @@
]
},
"name": {
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)",
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)",
"type": "string",
"enum": [
"3024",

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.34`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.35`",
"type": "object",
"properties": {
"animation": {
@@ -1284,7 +1284,7 @@
]
},
"layout_rules": {
"description": "Layout rules (default: None)",
"description": "Layout rules in the format of threshold => layout (default: None)",
"type": "object",
"additionalProperties": {
"type": "string",
@@ -1323,6 +1323,28 @@
}
]
},
"window_container_behaviour_rules": {
"description": "Window container behaviour rules in the format of threshold => behaviour (default: None)",
"type": "object",
"additionalProperties": {
"oneOf": [
{
"description": "Create a new container for each new window",
"type": "string",
"enum": [
"Create"
]
},
{
"description": "Append new windows to the focused window container",
"type": "string",
"enum": [
"Append"
]
}
]
}
},
"workspace_padding": {
"description": "Container padding (default: global)",
"type": "integer",
@@ -2245,7 +2267,7 @@
]
},
"name": {
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)",
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)",
"type": "string",
"enum": [
"3024",