Compare commits

...

114 Commits

Author SHA1 Message Date
LGUG2Z
1897e1845f feat(borders): permashow for floating windows
This commit leverages the z-ordering tracking of a border's target
window to enable permanently showing borders for floating windows while
applying different colours for focus state.
2024-12-09 08:23:03 -08:00
alex-ds13
192af6751b fix(border): stop removing borders on wrong monitors 2024-12-09 07:57:53 -08:00
LGUG2Z
4f306e5bfd docs(schema): update all json schemas 2024-12-07 16:43:28 -08:00
LGUG2Z
ede0b23bb4 feat(borders): track window movements + animations
This commit introduces a number of changes to the border manager module
to enable borders to track the movements of windows as they are being
animated.

As part of these changes, the code paths for borders to track user
movement of windows have also been overhauled.

The biggest conceptual change introduced here is borrowed from
@lukeyou05's work on tacky-borders, where the primary event listener of
the komorebi process now forwards EVENT_OBJECT_LOCATIONCHANGE and
EVENT_OBJECT_DESTROY messages from application windows directly on to
their borders.

These events are handled directly in the border window callbacks,
outside of the main border manager module event processing loop.

In order to handle these events more performantly in the border window
callbacks, a number of state trackers have been added to the Border
struct.

When handling EVENT_OBJECT_NAMECHANGE, these values are read directly
from the struct, whereas when handling WM_PAINT, which is sent by the
system whenever we invalidate a border window, we update the state
values on the Border structs from the various atomic configuration
variables in the mod.rs file.

Another trick I borrowed from tacky-borders is to store a pointer to the
Border object alongside a border window whenever it is created with
CreateWindowExW, which can be accessed within the callback as
GWLP_USERDATA.

There is some unfortunate introduction of unsafe code to make this
happen, but the callback uses null checks to exit the callback early to
ensure (to the best of my ability) that there are no pointer
dereferencing issues once we start making border changes in the context
of the callback.

There are a few other Direct2D related optimizations throughout this
commit, mainly avoiding the recreation of objects like brush properties
and brushes.

Finally, the border_z_order option is now deprecated as the border
window is now tracking the z-ordering of the application window it is
associated with by default - this should resolve a whole host of subtle
border z-ordering issues, especially when dragging windows around using
the mouse.

This work would not have been possible without the guidance of
@lukeyou05, so if you like this feature, please make sure you thank him
too!
2024-12-07 12:10:07 -08:00
LGUG2Z
e6b5b78857 feat(cli): add kill cmd
This commit adds a new komorebic command, "kill", to kill background
processes that may be started by "komorebic start", without terminating
the main komorebi process.

This is useful when iterating on changes to external components like the
bar which may require restarts.
2024-12-07 11:14:25 -08:00
LGUG2Z
440d78e8f4 feat(cli): add support for starting/stopping masir
This commit adds support to the start and stop commands for starting and
stopping masir.
2024-12-07 11:09:38 -08:00
LGUG2Z
280ca0ffcd perf(cli): validate komorebi proc earlier on start
This is a small change to the start command which moves the check for
the komorebi processes to come a little bit earlier.

This small change will make running commands like "komorebic start
--bar" around 3s faster when komorebi is already running.
2024-12-07 10:57:50 -08:00
LGUG2Z
f227bd0fef fix(cli): handle spaces in bar config paths
More PowerShell script username-with-spaces path handling shenanigans.

re #1161
2024-12-07 10:51:11 -08:00
LGUG2Z
3781c8ea41 fix(borders): update on resuming from suspend
This commit is a small fix to ensure that the new Direct2D borders get
redrawn properly after the operating system resumes from a suspended
state.
2024-12-02 16:35:11 -08:00
LGUG2Z
33800903f7 chore(deps): cargo update 2024-12-02 16:33:19 -08:00
CtByte
bb31e7155d feat(bar): komorebi widget visual changes
The visual changes include:

* the focused_window section is now indicating the active window in a stack and has hover effect.
* custom icons for all the layouts, including `paused`, `floating`, `monocle` states.
* custom layout/state picker with configurable options.
* display format configuration for the layouts (Icon/Text/IconAndText)
* display format configuration for the focused_window section (Icon/Text/IconAndText)
* display format configuration for the workspaces section (Icon/Text/IconAndText)
2024-12-02 16:12:51 -08:00
LGUG2Z
40b32332ae fix(wm): track all hwnds in known_hwnds 2024-11-30 15:41:57 -08:00
alex-ds13
8743cdd292 fix(wm): cross-monitor ops for floating windows
This commit fixes an issue where when trying to move floating windows or
windows on a floating workspace across boundaries to another monitor
using the `move_container_in_direction` it wouldn't move the floating
windows physically, although it moved them internally on komorebi,
resulting in weird and wrong behavior.

This commit creates a new method on `Monitor` to
`add_container_with_direction` which takes a move direction and then
uses the same logic that was previously on the
`move_container_in_direction` function.

It changes the `move_container_to_monitor` function to take an optional
move direction which if it is some will have this function call the new
method `add_container_with_direction` instead of just `add_container`.

Lastly the `move_container_in_direction` function now when it realizes
the move will be across monitors simply calls the
`move_container_to_monitor` with the direction that was initially given
to it.

These changes require that all callers of `move_container_to_monitor`
add an direction option, instead of passing `None` on all of them, a new
helper function was created, named `direction_from_monitor_idx` which
calculates the direction a move will have from the currently focused
monitor and the target monitor return `None` if they are the same or
returning `Some(direction)` if not. This way now all commands that call
a move across monitor will use the logic to check from the direction if
it should add the container on front or end.

With these changes now all the code related to moving a window across
monitors using a command should be on one place only making sure that in
the future any change required only needs to be done on one place,
instead of having to do it on `move_container_to_monitor` and
`move_container_in_direction` as before!

This commit is an interactive squashed rebase of the following commits
from PR #1154:

8f4bc101bc
fix(wm): move floats in direction across monitors

This commit fixes an issue where when trying to move floating windows or
windows on a floating workspace across boundaries to another monitor
using the `move_container_in_direction` it wouldn't move the floating
windows physically, although it moved them internally on komorebi,
resulting in weird and wrong behavior.

This commit creates a new method on `Monitor` to
`add_container_with_direction` which takes a move direction and then
uses the same logic that was previously on the
`move_container_in_direction` function.

It changes the `move_container_to_monitor` function to take an optional
move direction which if it is some will have this function call the new
method `add_container_with_direction` instead of just `add_container`.
Lastly the `move_container_in_direction` function now when it realizes
the move will be across monitors simply calls the
`move_container_to_monitor` with the direction that was initially given
to it.

These changes require that all callers of `move_container_to_monitor`
add an direction option, instead of passing `None` on all of them, a new
helper function was created, named `direction_from_monitor_idx` which
calculates the direction a move will have from the currently focused
monitor and the target monitor return `None` if they are the same or
returning `Some(direction)` if not. This way now all commands that call
a move across monitor will use the logic to check from the direction if
it should add the container on front or end.

3b20e4b2fe
refactor(wm): use helper function on move to workspace

Use the same `add_container_with_direction` function on
`move_container_to_workspace` as it is being used on
`move_container_to_monitor` or `move_container_in_direction`.

This way we bring parity between all methods and make it easier to
change the way a container is added on a monitor workspace when taking
the move direction into consideration.
2024-11-30 10:57:15 -08:00
alex-ds13
01367f59e2 fix(client): add write-timeout to prevent blocking 2024-11-29 08:11:36 -08:00
alex-ds13
e6ddccc5f6 fix(wm): move ops on floating workspaces
83f222fe84
* fix(wm): correctly define moves across monitors

Moves within the same workspace were being considered as moves across
monitors when the workspace was floating (not tiled).

This commit fixes this by changing the way we first define if a move was
across monitor or not.

We now search for the moved window on all workspaces and check if its
monitor index is different from the target monitor index (the monitor
where the move ended).

a02694348e
* fix(wm): ignore moves/resizes on floating workspaces

This commit makes sure that moves or resizes within a floating workspace
(i.e. not tiled) will be ignored, unless the move is across monitors.
We don't care about the positions or sizes of windows within a floating
workspace!

4bf24f81e0
* fix(wm): avoid workspace load on cross monitor moves

This commit replaces the `window_manager.focus_workspace` call with a
`monitor.focus_workspace` which doesn't load the workspace. There is no
need to load the workspace when moving windows across monitors since
those workspaces will already be loaded, we simply need to update them.
Loading the workspace would cause some issues as well, like when moving
a window to a floating workspace which already contained a window that
matched some `floating_windows` rules was always putting the
"floating_window" on top of the window we just moved with a bunch of
focus flickering. This is fixed with this commit.

cb53f463ae
* fix(wm): avoid workspace load on command move across monitor

If the move happens between the already focused workspaces of two
monitors we shouldn't load the workspace, since it is already loaded and
it will cause changes on focused windows, which might result on the
window we just moved not being focused.
2024-11-29 02:57:32 -08:00
LGUG2Z
b22ec90438 refactor(clippy): apply rust 1.83.0 lints 2024-11-28 12:12:46 -08:00
CtByte
46b81e4372 feat(bar): floating center area for widgets
* Added a new floating area at the center of the bar
* Optional center widgets config, fixed spacing on the center widget
* Turning transparency on by default
2024-11-26 20:14:01 -08:00
alex-ds13
6f00c527a4 fix(wm): cross-monitor max floating window moves
When moving maximized floating windows across monitors they were
magically disappearing!

The window would be on the correct place, with the correct coordinates
and size, its styles wouldn't change it would still have the `VISIBLE`
style, however the window was invisible.

If we used the system move to try to move it sometimes we would be able
to see a bar on the top of the monitor and if we moved the window with
the keyboard on the direction of another monitor then the window would
start showing up on that monitor... So it was visible on that monitor
but not on the one we just moved it into.

After some investigation I decided to atribute that behavior to magic,
since I couldn't find any other plausible explanation, if someone knows
about this please tell me, I too would like to learn the ways of this
dark mysteries from the deep of the Windows OS.

On a serious note, this commit creates a workaround for this by simply
unmaximazing the window first (it's not restore, it doesn't change the
size) then it moves the window (if animations are enabled it proceeds to
wait for the animation to finish...), then it maximizes the window
again.
2024-11-26 14:34:57 -08:00
alex-ds13
3ad4090df8 fix(wm): resize float windows moved across monitors
Previously when moving floating windows across monitors we would keep
the size of the window as it was. For most cases this would be ok.

However for users with monitors with completely different sizes this
could result on a window that would fill across monitors when moving
from the bigger monitor to the smaller monitor.

This commit, attempts to resize the windows proportionally to the
monitors' sizes.

There is currently a slight issue with some apps (so far I've only
noticed it on 'Wezterm'...) where if the DPIs across monitors are
different they don't seem to fully get the OS DPI change completely, but
it seems that setting the `Wezterm` compatibility high DPI scaling
override to "System" on the app's executable properties, fixes the
issue.

Since this is only 1 app (so far...) and only when the scales between
monitors are different I decided to commit this anyway.

This will do more good than harm, since in the cases it was misbehaving
with 'Wezterm' the result would be a wrongly resized window that is
still completely visible on the target monitor anyway and the override
fix seems to be good so far.
2024-11-26 14:34:17 -08:00
thearturca
449ccac645 refactor(animation): new animations engine
This commit is comprised of the following interactively rebased commits
from PR #1002 by @thearturca.

1a184a4442
refactor(animation): move animations to its own mod

First step for more rusty version animations. The goal is to make
animations more generic so its easier to add new animations to komorebi!

d3ac6b72c2
refactor(animation): reduce mutex calls on `ANIMATION_STYLE`

8a42b738fe
refactor(animation): introduce `Lerp` trait

e449861c10
refactor(animation): generalized ANIMATION_MANAGER

Instead of a isize key for the ANIMATION_MANAGER HashMap, now we use a
String key. For window move animation, the key would be
`window_move:{hwnd}`.

This allows us to use single manager for more types of animations.

67b2a7a284
feat(animation): introduce `AnimationPrefix` enum

8290f143a6
feat(animation): introduce `RenderDispatcher` trait

2400d757fe
feat(animation): implement window transparency animation

This commit also fixes graceful shutdown of animations by disabling them
before exit and wait for all remaining animations for 20 seconds.

44189d8382
refactor(animation): move generation of `animation key` to `RenderDispatcher`

e502cb3ffb
refactor(animation): rename `animation` mod to `engine`

Linter was upset about this:
> error: module has the same name as its containing module

369107f5e0
feat(config): adds per animation configuration options

Originally static config only allowed global config for animations.

Since this refactor introduces the abilty to add more type of
animations, this change allows us to configure `enabled`, `duration` and
`style` state per animation type.

Now each of them take either the raw value or a JSON object where keys
are the animation types and values are desired config value. Also adds
support for per animation configuration for komorebic commands.
2024-11-25 20:42:56 -08:00
dependabot[bot]
1d00196240 chore(deps): bump serde from 1.0.214 to 1.0.215
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.214 to 1.0.215.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.214...v1.0.215)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:27:34 -08:00
dependabot[bot]
639ebd0b3d chore(deps): bump serde_json from 1.0.132 to 1.0.133
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.132 to 1.0.133.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.132...v1.0.133)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:27:27 -08:00
dependabot[bot]
e22eafbc8e chore(deps): bump clap from 4.5.20 to 4.5.21
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.20 to 4.5.21.
- [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.20...clap_complete-v4.5.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:27:19 -08:00
dependabot[bot]
46c2ad512b chore(deps): bump rustls from 0.23.16 to 0.23.18
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.16 to 0.23.18.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.16...v/0.23.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:26:59 -08:00
LGUG2Z
3de96609bb feat(bar): support floating window title updates 2024-11-25 15:08:08 -08:00
LGUG2Z
9c09284b0f fix(subscriptions): add override for title updates 2024-11-25 14:48:36 -08:00
CtByte
b14c0d07a2 fix(bar): correct network widget icon colour 2024-11-23 13:54:36 -08:00
alex-ds13
3615451f41 fix(wm): disallow focusing other windows when there is a maximized_window 2024-11-21 12:06:54 -08:00
alex-ds13
6893f39dd9 fix(wm): focus maximized windows when moving focus across monitors
There was an issue where if you changed focus across monitors and the
target monitor had a maximized window it would focus one of the
containers beneath it instead. And if there were no containers it
wouldn't focus anything, but instead keep focus on the previous monitor.
This commit fixes that issue.
2024-11-21 12:06:54 -08:00
LGUG2Z
041ef5731c refactor(bar): use monitor idx when switching ws
This commit follows up on a point made by @notTamion in #1128 - since we
have the monitor index, we can use it in the bar's workspace widget to
more accurately target workspaces via
SocketMessage::FocusMonitorWorkspaceNumber.
2024-11-19 20:44:48 -08:00
LGUG2Z
779c12bc6a docs(schema): update all json schemas 2024-11-19 19:52:33 -08:00
LGUG2Z
0e48370b73 style(bar): add aliases for default grouping style
This commit adds the "CtByte" and "CtByteWithShadow" aliases for the
"Default" and "DefaultWithShadow" GroupingStyle variants respectively as
an easter egg to recognize @CtByte's work in implementing the grouping
feature.
2024-11-19 19:48:10 -08:00
LGUG2Z
8de92ec32a docs(bar): add a position.end.y val warning
For dummies like me who set this to 0.0 and then wonder why the bar
disappears on config change.
2024-11-19 19:42:36 -08:00
LGUG2Z
ac243c6992 fix(bar): read latest transparency_alpha value
This commit is a small fix which ensures that the latest value of
transparency_alpha will be read and applied from config rather than
self.config in the apply_config fn.
2024-11-19 19:33:02 -08:00
LGUG2Z
d001d8a7a6 fix(wm): ensure focus restore on float + max hwnds
This commit ensures that when switching to a workspace which contains a
floating window or a maximized window, either the maximized window or
the first floating window index will be focused.

This is to prevent windows which have been hidden on the previous
workspace from retaining keyboard focus.

fix #1130
2024-11-19 19:01:38 -08:00
CtByte
219fa8e14f feat(bar): add widget grouping options
This commit adds various widget grouping and transparency options to
komorebi-bar, and is comprised of the individual commits listed below,
worked on in PR #1108, squashed into one.

e8f5952abb
* adding RenderConfig, and some test frames on widgets

0a5e0a4c0a
* no clone

a5a7d6906c
* comment

6a91dd46cd
* ignore unused

80f0214e47
* Group enum, Copy RenderConfig

fbe5e2c1f7
* Group -> Grouping

ce49b433f9
* GroupingConfig

f446a6a45f
* "fmt --check" fix (thanks VS)

d188222be7
* added widget grouping and group module

1008ec2031
* rounding from settings, and apply_on_side

7fff6d29a9
* dereferencing

655e8ce4c1
* AlphaColour, transparency, bar background, more grouping config options

cba0fcd882
* added RoundingConfig

ec5f7dc82d
* handling grouping edge case for komorebi focus window

12117b832b
* changed default values

645c46beb8
* background color using theme color, AlphaColour.to_color32_or, updating json format for Grouping and RoundingConfig

10d2ab21c7
* hot-reload on grouping

d88774328a
* grouping correction on init

2cd237fd0d
* added shadow to grouping, optional width on grouping stroke

4f4b617f26
* grouping on bar, converting AlphaColour from_rgba_unmultiplied, simplified grouping

3808fcec8f
* widget rounding based on grouping, atomic background color, simplified config, style on grouping

be45d14f6d
* renamed Side to Alignment, group spacing

be45d14f6d
* proper widget spacing based on alignment

b43a5bda69
* added widget_spacing to config

c18e5f4dbe
* test commit

cba2b2f7ac
* refactoring of render and grouping, widget spacing WIP

9311cb00ec
* simplify no_spacing

36c267246b
* correct spacing on komorebi and network widgets (WIP)

85a41bf5b2
* correct widget spacing on all widgets

50b49ccf69
* refactoring widget spacing

9ec67ad988
* account for ui item_spacing when setting the widget_spacing

e88a2fd9c0
* format
2024-11-18 19:56:00 -08:00
LGUG2Z
e4e94fd1a6 feat(borders): use direct2d for anti-aliasing
This commit overhauls the "Komorebi" borders implementation to use
Direct2D, which enables anti-aliasing for rounded borders.

A lot of the heavy lifting was done by @lukeyou05 in the tacky-borders
project, which this commit largely adapts to komorebi. @lukeyou05
provided an incredible amount of guidance and feedback on the
implementation of this feature on the komorebi Discord.

This commit is a squashed interactive rebase of the following commits:

238271a71e
feat(borders): initial impl of direct2d border drawing

5525a382b9
feat(borders): avoid multiple render target creation calls

431970d7b6
feat(borders): reduce redraws to improve perf

47cb19e54a
feat(borders): remove black pixels around direct2d corners

3857d1a46c
feat(borders): clean up render targets on destroy
2024-11-17 10:57:57 -08:00
LGUG2Z
e707a14b8a docs(readme): add gazafunds link 2024-11-17 10:09:03 -08:00
dependabot[bot]
818ec1c63b chore(deps): bump catppuccin-egui from 5.3.0 to 5.3.1
Bumps [catppuccin-egui](https://github.com/catppuccin/egui) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/catppuccin/egui/releases)
- [Changelog](https://github.com/catppuccin/egui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/catppuccin/egui/compare/catppuccin-egui-v5.3.0...catppuccin-egui-v5.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 14:46:54 -08:00
dependabot[bot]
a10bb467e5 chore(deps): bump thiserror from 1.0.68 to 2.0.3
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.68 to 2.0.3.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.68...2.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 14:46:44 -08:00
dependabot[bot]
4fd60bbff3 chore(deps): bump image from 0.25.4 to 0.25.5
Bumps [image](https://github.com/image-rs/image) from 0.25.4 to 0.25.5.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.4...v0.25.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 14:46:34 -08:00
LGUG2Z
cc196db046 feat(cli): add cycle-stack-index cmd
This commit adds a new komorebi command "cycle-stack-index" which allows
the user to manipulate the index position of the focused window in the
focused stack by swapping it with either the previous or the next window
until the desired index position has been found.
2024-11-11 16:27:45 -08:00
alex-ds13
7f0b54c35e fix(wm): add read timeout to command socket
After investigating further the issue where commands would randomly stop
working, we've noticed that the issue seems to be that somehow the
listening thread gets stuck reading the unix socket, as in it
continuously tries to read a socket on a connection that is not sending
anything anymore. The result would be that komorebi would no longer be
able to receive commands until it was restarted.

This fix adds a read timeout of 1s and it spawns a new thread to handle
the stream reading and process of cmds. So in case this happens again,
that specific processing thread will only be stuck for 1s but the rest
of komorebi will never get stuck and should keep working normally.
2024-11-11 06:22:42 -08:00
thearturca
b1726af2eb fix(animation): set pos for all container windows
This change prevents the move animation from playing again for each
window in the stack. Tested with all hiding behaviors. Looks good so
far.

resolve #1029
2024-11-10 09:24:56 -08:00
LGUG2Z
fd8cd4bb01 docs(github): add pull request template 2024-11-08 12:34:47 -08:00
LGUG2Z
172988ed81 fix(wm): apply ws cfgs only to declared ws indices
This commit ensures that when attempting to reload the static
configuration file after the user has imperatively created new
workspaces, the configuration reload logic will not attempt to load a
workspace config for those imperatively created workspaces.
2024-11-08 12:28:52 -08:00
LGUG2Z
36e3eaad36 fix(bar): retain exact workspace indices
This commit ensures that the exact workspace indices are tracked in the
komorebi widget state.

This fixes a bug where an incorrect workspace index could be sent with
SocketMessage::FocusWorkspaceNumber if a user had hide_empty_workspaces
set to true.

fix #1102
2024-11-04 17:01:49 -08:00
LGUG2Z
0f022d47df feat(cli): add close-workspace cmd
This commit introduces a new komorebic command, close-workspace. This
command will remove the focused workspace from the window manager state
if the following conditions are met:

1. The number of workspaces on the focused monitor are >1
2. The workspace is empty
3. The workspace is unnamed

The third condition is to ensure that we are not removing workspaces
which have been declared in the static configuration file.
2024-11-04 12:39:08 -08:00
LGUG2Z
dc1eb8ff50 fix(cli): expand list of ahk executable names
fix #1103
2024-11-04 12:07:38 -08:00
LGUG2Z
166f505aba fix(cli): handle spaces in bar config paths 2024-11-04 08:01:39 -08:00
LGUG2Z
d55d356b37 chore(dev): begin v0.1.31-dev 2024-11-04 07:58:49 -08:00
LGUG2Z
9a3dbccc89 chore(release): v0.1.30 2024-11-03 15:43:22 -08:00
LGUG2Z
24b43a154c fix(wm): remove panic on missing matching strategy
This commit ensures that if an IdWithIdentifer without an explicitly set
matching strategy makes it through to should_act_individual, it will be
treated the same as MatchingStrategy::Legacy instead of causing a
runtime panic.
2024-11-03 08:22:13 -08:00
LGUG2Z
6a09ec4b87 feat(cli): update start cmd output blurbs 2024-11-01 20:53:06 -07:00
LGUG2Z
374cd2c6d5 feat(wm): switch to asc v2 json format
This commit switches all relevant commands to treat the v2
applications.json asc format as the default format in all commands.

The v1 applications.yaml file will still be processed correctly if
passed.
2024-11-01 19:43:04 -07:00
alex-ds13
0f385d6245 fix(wm): restore hidden windows correctly
Fixes the issue talked on Discord[1] here where when using `Hide` as
hiding behaviour some windows were hidden and never restored.

The same would happen if using stacks with apps that matched a
tray_and_multi_window_identifiers rule.

[1]: https://discord.com/channels/898554690126630914/898554690608967786/1301581412008202298
2024-11-01 19:00:26 -07:00
alex-ds13
5503323695 fix(wm): handle moving windows to/from floating workspaces
This commit fixes the issue related to moving windows to/from a floating
workspace to a tiled workspace.

Previously the start of the move would be ignored however when moving
back from a tiled workspace since it didn't know about the existance of
that window it would also "move" that workspace focused tiled window
without physically moving it, leaving it in a weird state that seemed
like it was unmanaged.

This commit changes the way this mouse moves are handled and now also
handles moving `floating_windows` and even monocle or maximized windows.
2024-11-01 15:50:22 -07:00
LGUG2Z
aa9f50fd5c docs(readme): add link to awesome komorebi list 2024-11-01 14:17:19 -07:00
LGUG2Z
3a6ae01b12 fix(borders): permit failure on global destruction
This commit allows calls to Border::destroy to fail when called in the
context of border_manager::destroy_all_borders. This is important in the
context of the retile command, which calls this function, to not leave
the retile in an inconsistent state.
2024-11-01 11:53:21 -07:00
LGUG2Z
720089587b docs(readme): add to license explanation re: nonpersonal use 2024-11-01 09:38:43 -07:00
LGUG2Z
852d1f9eb0 docs(bar): update egui docstring link
Thanks to @dinesh-58 for pointing this out!

resolve #1078
2024-10-31 17:12:27 -07:00
LGUG2Z
ed5b0f9120 fix(wm): avoid slurping on stack -> stack ops
This commit ensures that when both the origin and target containers are
stacks during a stack operation, the "slurping" stack extension
behaviour introduced in cfb0c7f2ce will
not be applied.

fix #1085
2024-10-31 16:58:46 -07:00
alex-ds13
7f7b8c7c05 fix(borders): update border loc when moving
Currently, komorebi checks if a move is happening by checking if the
left mouse is pressed and updates the borders when there is a move while
the left mouse button is pressed (BTW this is why when moving with the
keyboard using the system move it only updates after pressing enter).

However, for some reason AltSnap somehow steals this left button
information and komorebi thinks the button is not pressed.

This PR makes it so it checks for the state of the pending_move_op and
keeps updating the borders while this is_some().

This fixes both that issue with AltSnap and the issue with system move,
as well as any other situations that might allow moving a window with
anything else that doesn't use a left mouse button press.
2024-10-31 12:39:32 -07:00
alex-ds13
4198f6fabc fix(wm): set last focused workspace on alt-tab 2024-10-30 09:59:04 -07:00
alex-ds13
4acd408b88 fix(wm): focus single window on show event
This commit makes sure we refocus the window on `Show` event when it is
the only window on the workspace.

This is needed because some windows send the `FocusChange` event before
the `Show` event and on the first event we will be focusing the desktop
window to unfocus any previous window from other workspace because the
workspace will still be empty. So after adding the window, we need to
focus it again.
2024-10-28 16:17:25 -07:00
alex-ds13
73d928771d fix(wm): handle monocled stack cycle commands 2024-10-28 16:16:59 -07:00
alex-ds13
d194afe6e6 docs(mkdocs): note that grid layout doesn't support resizing 2024-10-28 16:14:24 -07:00
Carlos Regis
72f4ed0575 docs(mkdocs): update border property name 2024-10-28 16:13:11 -07:00
dependabot[bot]
82ff69e337 chore(deps): bump shadow-rs from 0.35.1 to 0.35.2
Bumps [shadow-rs](https://github.com/baoyachi/shadow-rs) from 0.35.1 to 0.35.2.
- [Release notes](https://github.com/baoyachi/shadow-rs/releases)
- [Commits](https://github.com/baoyachi/shadow-rs/compare/v0.35.1...v0.35.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 13:26:53 -07:00
dependabot[bot]
583d8b1e4c chore(deps): bump regex from 1.11.0 to 1.11.1
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.0...1.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 13:26:43 -07:00
dependabot[bot]
2f1f1b160c chore(deps): bump serde from 1.0.210 to 1.0.213
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.210 to 1.0.213.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.210...v1.0.213)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 13:26:36 -07:00
dependabot[bot]
bcefea8a1a chore(deps): bump serde_json from 1.0.128 to 1.0.132
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.128 to 1.0.132.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/1.0.128...1.0.132)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 13:26:27 -07:00
dependabot[bot]
223404a1aa chore(deps): bump image from 0.25.3 to 0.25.4
Bumps [image](https://github.com/image-rs/image) from 0.25.3 to 0.25.4.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.3...v0.25.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 13:26:17 -07:00
LGUG2Z
7d032db507 ci(github): add missing multiline backslash 2024-10-28 13:06:50 -07:00
alex-ds13
09afad624f fix(wm): correct monitor index preference handling 2024-10-18 10:29:54 -07:00
alex-ds13
903b6af507 feat(wm): apply workspace-rules to floating windows
This commit allows `workspace-rules` and `initial_workspace_rules` to be
applied to floating windows. As a by product of this commit, now the
command to show `visible-windows` will now also show the maximized
windows, monocled windows and floating windows.
2024-10-18 09:59:44 -07:00
LGUG2Z
1644fcf6f2 ci(github): add note about ghost tiles on bug template 2024-10-17 15:46:28 -07:00
LGUG2Z
b31e8911ce docs(mkdocs): add phantom tiles troubleshooting 2024-10-17 15:36:18 -07:00
alex-ds13
298fbafe00 feat(wm): add floating app info to ruledebug
This commit adds a `matches_floating_applications` to the `RuleDebug`
which allows users to know if a window was matched as a floating window
when using the debug part of the GUI.
2024-10-16 16:17:58 -07:00
LGUG2Z
e46a1a6117 ci(github): add sponsor check 2024-10-16 16:15:27 -07:00
LGUG2Z
34929f32a7 feat(wm): add theme socket message
This commit adds a new SocketMessage::Theme which allows for themes to
be set programmatically. This change has also been plumbed through to
komorebi-bar so that the bar theme will also update after komorebi
processes the message and passes it on to subscribers.

A new theme_manager module has been introduced to add notification-based
handling of theme changes, both from the static config file being
updated and from SocketMessage::Theme being received.
2024-10-16 15:10:34 -07:00
LGUG2Z
1ef7a09163 docs(mkdocs): generate latest cli docs 2024-10-16 09:14:00 -07:00
Arnaud Künzi
c0c3c81d69 docs(mkdocs): add more info on whkdrc config 2024-10-15 17:03:53 -07:00
Adinelson Brühmüller
7276dc2309 feat(cli): add --ahk flag to stop cmd
resolve #951
2024-10-15 17:03:53 -07:00
LGUG2Z
58d3086615 ci(github): update build + release workflow
This commit updates the build and release workflow to enable multi-arch
builds and releases.

A number of Rust-specific actions have been added, namely rust-cache to
handle cargo caching and actions-rust-cross to handle cross-compilation.

A release-dry-run target has been added to run on master which should
help catch any issues in release workflow changes early.

Releases drop goreleaser entirely in favour of action-gh-release which
was already in use to add msi installers to the releases previously
created by goreleaser.
2024-10-15 17:03:50 -07:00
Dovie Weinstock
aa5a36989f fix(cli): escape start --ahk args w/ double quotes
fix #1040
2024-10-14 17:29:33 -07:00
LGUG2Z
b612066367 feat(config): add support for v2 asc json
This commit adds support for a v2 format of the application specific
configuration file, centralizing on JSON to maximize the knowledge
crossover for people already familiar with the types used in
komorebi.json.

The biggest difference besides the format change is that matchers must
be used explicitly for every kind of rule, rather than being able to
specify options on a default rule. This is a bit more verbose, but
ultimately allows for significantly more flexibility.
2024-10-14 16:50:03 -07:00
dependabot[bot]
cbe5b24f73 chore(deps): bump egui-phosphor from 0.7.2 to 0.7.3
Bumps [egui-phosphor](https://github.com/amPerl/egui-phosphor) from 0.7.2 to 0.7.3.
- [Changelog](https://github.com/amPerl/egui-phosphor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amPerl/egui-phosphor/compare/v0.7.2...v0.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 07:21:15 -07:00
LGUG2Z
2ead216eeb chore(just): add install-targets to justfile 2024-10-13 15:07:31 -07:00
LGUG2Z
b0944662fa fix(wm): add cmd thread lock acquisition timeouts
After some investigation by @alex-ds13 on Discord it looks like there
are times where attempting to gain a lock on the WindowManager inside of
read_commands_uds results in the thread becoming blocked when it's not
possible to obtain the lock.

Instead of waiting indefinitely for a lock, this change ensures that we
will wait for at most 1 second before discarding the message so that the
command listener loop can continue.

Warning logs have been added to inform when a message has been dropped
as a result of lock acquisition failure.
2024-10-13 15:07:31 -07:00
LGUG2Z
1376d7be04 feat(wm): add retile with resize socket msg
This commit adds a new SocketMessage variant,
RetileWithResizeDimensions, to preserve any resize dimensions applied by
the user.

This new variant is now used when clicking on a workspace using the
komorebi widget in komorebi-bar.
2024-10-13 15:07:31 -07:00
LGUG2Z
5da72e10df feat(client): add subscribe_with_options
This commit adds a new method, subscribe_with_options to
komorebi-client.

The first option introduced is to tell komorebi to only send
notifications when the window manager state has been changed during the
processing of an event.

This new subscription option is now used with komorebi-bar to improve
rendering and update performance.
2024-10-13 15:07:31 -07:00
LGUG2Z
d21ffb28cc fix(cli): update fetch-asc output to use '/' 2024-10-13 15:07:31 -07:00
LGUG2Z
4e27febdc0 chore(cargo): suppress macro lint warnings 2024-10-13 15:07:31 -07:00
LGUG2Z
3c5852ae20 docs(cli): highlight eol features in start + check 2024-10-13 15:07:27 -07:00
LGUG2Z
7943fccb1b chore(cargo): +nightly fmt 2024-10-13 10:05:11 -07:00
LGUG2Z
2d2cea31c0 docs(schema): update all json schemas 2024-10-13 10:05:11 -07:00
alex-ds13
a17924c2af feat(config): add floating border colour opt 2024-10-13 10:05:11 -07:00
alex-ds13
91519227d4 fix(wm): allow cross-monitor floating window moves
This commit changes the `move_container_to_monitor` from the WM to allow
moving floating windows as well.

It also adds a new method `move_to_area` to the `Window` that allows
moving a window from one monitor to another keeping its size.
2024-10-13 10:05:11 -07:00
alex-ds13
f91d0aabf5 fix(wm): check exhaustively for ws emptiness
This commit creates a new function for the workspaces to check if they
are empty or not.

This function properly accounts for maximized windows, monocle windows
and floating windows.

This should fix the cases where the WM was checking if the workspace was
empty to focus the desktop in order to loose focus from previously
focused window.

Previously it wasn't checking for floating windows so it cause continues
focus flickering when there were only floating windows on the workspace.
2024-10-13 10:05:11 -07:00
alex-ds13
d5b6584042 feat(wm): add float override option
This commit introduces a new option `float_override`, which makes it so
every every window opened, shown or uncloaked will be set to floating,
but it won't be ignored. It will be added to the floating_windows of the
workspace, meaning that the user can later tile that window with
toggle-float command.

This allows the users to have all windows open as floating and then
manually tile the ones they want.

This interactively rebased commit contains changes from the following
individual commits:

0e8dc85fb1
feat(wm): add new float override option

30bdaf33d5
feat(cli): add command for new option `ToggleFloatOverride`

b7bedce1ca
feat(wm): add window_container_behaviour and float_override to workspaces

221e4ea545
feat(cli): add commands for workspace new window behaviour and float_override

b182cb5818
fix(wm): show floating apps in front of stacked windows as well

7c9cb11a9b
fix(wm): Remove unecessary duplicated code
2024-10-13 10:05:11 -07:00
LGUG2Z
6db317d425 feat(wm): separate floating and ignored apps
This commit introduces a distinction between ignored applications
(previously identified with float_rules) and floating applications.

All instances of "float_" with the initial meaning of "ignored" have
been renamed with backwards compatibility aliases.

Floating applications will be managed under Workspace.floating_windows
if identified using a rule, and this allows them to now be moved across
workspaces.

A new border type has been added for floating applications, and the
colour can be configured via theme.floating_border.

This interactively rebased commit contains changes from the following
individual commits:

17ea1e6869
feat(wm): separate floating and ignored apps

8b344496e6
feat(wm): allow ws moves of floating apps

7d8e2ad814
refactor(wm): float_rules > ignore_rules w/ compat

d68346a640
fix(borders): no redraws on floating win title change

a93e937772
fix(borders): update on floating win drag

68e9365dda
fix(borders): send notif on ignored hwnd events
2024-10-13 10:05:11 -07:00
Csaba
07a1538905 feat(bar): add label prefix config opt
This commit makes the label prefix configurable. Users can select if
they want to show an icon, only text, or both text and an icon.
2024-10-13 10:05:11 -07:00
LGUG2Z
853db2f15f docs(github): update issue templates 2024-10-13 10:05:11 -07:00
LGUG2Z
c9f180ce0f chore(deps): cargo update 2024-10-13 10:05:11 -07:00
LGUG2Z
bc00f54c90 feat(bar): add more logging around error paths 2024-10-13 10:05:11 -07:00
LGUG2Z
c57759242a feat(wm): delete stale sub socket files 2024-10-13 10:05:11 -07:00
LGUG2Z
6f27de8e5c chore(deps): bump eframe from 0.28 to 0.29 2024-10-13 10:05:11 -07:00
Csaba
a9e98034b0 feat(bar): add cpu widget
This commit adds a CPU widget, following the patterns of the Memory
widget.
2024-10-13 10:05:11 -07:00
LGUG2Z
3489163793 refactor(wm): standardize config env var handling
This commit ensures that whenever komorebi.json is read and deserialized
into StaticConfig via StaticConfig::read, all known paths where
$Env:KOMOREBI_CONFIG_HOME and $Env:USERPROFILE are accepted will be run
through the resolve_home_path helper fn.
2024-10-13 10:05:11 -07:00
LGUG2Z
71d65cf4a1 fix(wm): ignore minimize calls on komorebi-bar
Hopefully I don't have to make this yet another configurable list...
2024-10-13 10:05:10 -07:00
alex-ds13
a5e6828d1e fix(wm): update monitor focus before focus-stack-window
This commit fixes the cases where you'd call this command on a monitor
which was not focused, for example by pressing a button on a bar like
komorebi-bar or other when you had focus on another monitor.
This change ensures that first we focus the monitor where the mouse cursor
is, this way it will act on the monitor that you've just pressed instead
of the monior that was focused before.
2024-10-13 10:05:10 -07:00
LGUG2Z
96605f72a7 feat(config): add bar configurations opt
This commit adds a "bar_configurations" option to the static config file
which takes an array of PathBufs.

If this option is defined and the --bar flag is passed to the "komorebic
start" command, komorebic will attempt to launch multiple instances of
komorebi-bar.exe with the --config flag pointing to the PathBufs given.

This configuration option is only consumed by komorebic, not by the
window manager directly, so it could also be used by other status bar
projects to read configuration file locations from.

There is no requirement for the PathBufs to point specifically to
komorebi bar configuration files if the --bar flag is not being used
with "komorebic start".
2024-10-13 10:05:10 -07:00
LGUG2Z
18bb060b71 refactor(bar): use native apis for positioning
This commit replaces almost all uses of the egui Viewport API for bar
window positioning with calls to SetWindowPos via komorebi_client's
Window struct.

This seems to play much more smoothly with multi-monitor setups where
each monitor has a different scaling factor, opening the door for
multiple instances of komorebi-bar.exe to run against multiple monitors.

As a result of this change, the "viewport" configuration option has been
renamed to "position" and doc strings have been changed to remove the
reference to the egui crate docs. Similarly, "viewport.position" and
"viewport.inner_size" have been renamed to "position.start" and
"position.end" respectively. Backwards-compatibility aliases have been
included for all renames.
2024-10-13 10:05:10 -07:00
112 changed files with 9921 additions and 4031 deletions

View File

@@ -1,52 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]: Short descriptive title"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See bug
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots and Videos**
Add screenshots and videos to help explain your problem.
**Operating System**
Provide the output of `systeminfo | grep "^OS Name\|^OS Version"`
For example:
```
OS Name: Microsoft Windows 11 Pro
OS Version: 10.0.22000 N/A Build 22000
```
**`komorebic check` Output**
Provide the output of `komorebic check`
For example:
```
No KOMOREBI_CONFIG_HOME detected, defaulting to C:\Users\LGUG2Z
Looking for configuration files in C:\Users\LGUG2Z
No komorebi configuration found in C:\Users\LGUG2Z
If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration
```
**Additional context**
Add any other context about the problem here.
In particular, if you have any other AHK scripts or software running that handle any aspect of window management or manipulation

64
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Bug report
description: File a bug report
labels: [ bug ]
title: "[BUG]: "
body:
- type: markdown
attributes:
value: |
Please **do not** open an issue for applications with invisible windows leaving ghost tiles.
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers , then this is not a bug with komorebi but a bug with the application you are using, and should open an issue with the developer(s) of that application.
- type: textarea
validations:
required: true
attributes:
label: Summary
description: >
Please provide a short summary of the bug, along with any information
you feel is relevant to replicating the bug.
You may include screenshots and videos in this section.
- type: textarea
validations:
required: true
attributes:
label: Version Information
description: >
Please provide information about the versions of Windows and komorebi
running on your machine.
Do not submit a bug if you are not using an official version of Windows
such as AtlasOS; only official versions of Windows are supported.
```
systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version"
```
```
komorebic --version
```
- type: textarea
validations:
required: true
attributes:
label: Komorebi Configuration
description: >
Please provide your configuration file (komorebi.json or komorebi.bar.json)
render: json
- type: textarea
validations:
required: true
attributes:
label: Hotkey Configuration
description: >
Please provide your whkdrc or komorebi.ahk hotkey configuration file
- type: textarea
validations:
required: true
attributes:
label: Output of komorebic check
description: >
Please provide the output of `komorebic check`

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Komorebi Documentation
url: https://lgug2z.github.io/komorebi/
about: Please search the documentation website before opening an issue
- name: Komorebi Quickstart Tutorial Video
url: https://www.youtube.com/watch?v=MMZUAtHbTYY
about: If you are new, please make sure you watch the quickstart tutorial video

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEAT]: Short descriptive title"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,37 @@
name: Feature request
description: Suggest a new feature (Sponsors only)
labels: [enhancement]
title: "[FEAT]: "
body:
- type: dropdown
id: Sponsors
attributes:
label: Sponsorship Information
description: >
Feature requests are considered from individuals who are $5+ monthly sponsors to the project.
Please specify the platform you use to sponsor the project.
options:
- GitHub Sponsors
- Ko-fi
- Discord
- YouTube
default: 0
validations:
required: true
- type: textarea
validations:
required: true
attributes:
label: Suggestion
description: >
Please share your suggestion here. Be sure to include all necessary context.
If you sponsor on a platform where you use a different username, please specify the username here.
- type: textarea
validations:
required: true
attributes:
label: Alternatives Considered
description: >
Please share share alternatives you have considered here.

7
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,7 @@
<!--
Please follow the Conventional Commits specification.
If you need to update your PR with changes from `master`, please run `git rebase master`.
By opening this PR, you confirm that you have read and understood this project's `CONTRIBUTING.md`.
-->

125
.github/workflows/sponsor-check.yaml vendored Normal file
View File

@@ -0,0 +1,125 @@
name: Feature Request Sponsor Check
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
test_username:
description: "Test username to check sponsorship for"
required: true
default: "octocat"
test_title:
description: "Test issue title"
required: true
default: "[FEAT] Test Feature Request"
test_sponsor_platform:
description: "Selected sponsor platform"
required: true
type: choice
options:
- "GitHub Sponsors"
- "Ko-fi"
- "Discord"
- "YouTube"
jobs:
check-sponsor:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_dispatch') || (github.event_name == 'issues' &&
startsWith(github.event.issue.title, '[FEAT]') &&
github.event.issue.user.login != 'LGUG2Z' &&
fromJSON(github.event.issue.body).Sponsors == 'GitHub Sponsors')
steps:
- name: Get Issue Details
id: issue-details
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "username=${{ github.event.inputs.test_username }}" >> $GITHUB_OUTPUT
echo "title=${{ github.event.inputs.test_title }}" >> $GITHUB_OUTPUT
echo "sponsor_platform=${{ github.event.inputs.test_sponsor_platform }}" >> $GITHUB_OUTPUT
else
echo "username=${{ github.event.issue.user.login }}" >> $GITHUB_OUTPUT
echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
echo "sponsor_platform=$(jq -r '.Sponsors' <<< '${{ github.event.issue.body }}')" >> $GITHUB_OUTPUT
fi
- name: Get Sponsorship Status
id: sponsorship
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PAT }}
script: |
const username = '${{ steps.issue-details.outputs.username }}';
const sponsorPlatform = '${{ steps.issue-details.outputs.sponsor_platform }}';
if (sponsorPlatform !== 'GitHub Sponsors') {
console.log('Sponsor platform is not GitHub Sponsors, skipping check');
return true;
}
const sponsorshipQuery = `query($user: String!) {
user(login: $user) {
... on Sponsorable {
sponsorshipForViewerAsSponsorable {
tier {
name
monthlyPriceInDollars
}
}
}
}
}`;
try {
const result = await github.graphql(sponsorshipQuery, {
user: username
});
console.log(result);
const sponsorship = result.user.sponsorshipForViewerAsSponsorable;
console.log(sponsorship);
const amount = sponsorship?.tier?.monthlyPriceInDollars || 0;
console.log(`Sponsorship amount for ${username}: $${amount}/month`);
return amount >= 5;
} catch (error) {
console.log(`Error checking sponsorship: ${error.message}`);
return false;
}
- name: Print Test Results
if: github.event_name == 'workflow_dispatch'
run: |
echo "Test Results for ${{ steps.issue-details.outputs.username }}:"
echo "Title: ${{ steps.issue-details.outputs.title }}"
echo "Platform: ${{ steps.issue-details.outputs.sponsor_platform }}"
echo "Would close issue: ${{ steps.sponsorship.outputs.result == 'false' }}"
- name: Close Issue If Not Sponsor
if: |
github.event_name == 'issues' &&
steps.sponsorship.outputs.result == 'false'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issueNumber = context.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: 'Thank you for your feature request! This repository requires a GitHub sponsorship of at least $5/month to submit feature requests. Please consider becoming a sponsor at https://github.com/sponsors/LGUG2Z'
});
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: 'closed'
});

View File

@@ -15,109 +15,88 @@ on:
- v*
schedule:
- cron: "30 0 * * 0" # Every day at 00:30 UTC
workflow_dispatch:
jobs:
build:
name: Build
runs-on: windows-latest
strategy:
fail-fast: true
matrix:
platform:
- os-name: Windows-x86_64
runs-on: windows-latest
target: x86_64-pc-windows-msvc
- os-name: Windows-aarch64
runs-on: windows-latest
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.platform.runs-on }}
permissions: write-all
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
matrix:
target:
- x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Prep cargo dirs
run: |
New-Item "${env:USERPROFILE}\.cargo\registry" -ItemType Directory -Force
New-Item "${env:USERPROFILE}\.cargo\git" -ItemType Directory -Force
shell: powershell
- name: Set environment variables appropriately for the build
run: |
echo "%USERPROFILE%\.cargo\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
echo "TARGET=${{ matrix.target }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
echo "SKIP_TESTS=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
- name: Cache cargo registry, git trees and binaries
uses: actions/cache@v4
- run: rustup toolchain install stable --profile minimal
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Get rustc commit hash
id: cargo-target-cache
run: |
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
shell: bash
- name: Cache cargo build
uses: actions/cache@v4
cache-on-failure: "true"
cache-all-crates: "true"
key: ${{ matrix.platform.target }}
- run: cargo +nightly fmt --check
- run: cargo clippy
- uses: houseabsolute/actions-rust-cross@v0
with:
path: target
key: ${{ github.base_ref }}-${{ github.head_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ github.base_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install Rustup using win.rustup.rs
run: |
# Disable the download progress bar which can cause perf issues
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
shell: powershell
- name: Ensure stable toolchain is up to date
run: rustup update stable
shell: bash
- name: Install the target
run: |
rustup target install ${{ matrix.target }}
- name: Run Cargo checks
run: |
cargo fmt --check
cargo check
cargo clippy
- name: Run a full build
run: |
cargo build --locked --release --target ${{ matrix.target }}
- name: Create MSI installer
run: |
command: "build"
target: ${{ matrix.platform.target }}
args: "--locked --release"
- run: |
cargo install cargo-wix
cargo wix -p komorebi --nocapture -I .\wix\main.wxs --target x86_64-pc-windows-msvc
- name: Upload the built artifacts
uses: actions/upload-artifact@v4
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: komorebi-${{ matrix.target }}
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: |
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebic-no-console.exe
target/${{ matrix.target }}/release/komorebi-bar.exe
target/${{ matrix.target }}/release/komorebi-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
target/${{ matrix.target }}/release/komorebi_gui.pdb
target/${{ matrix.platform.target }}/release/*.exe
target/${{ matrix.platform.target }}/release/*.pdb
target/wix/komorebi-*.msi
retention-days: 7
- name: Check GoReleaser
uses: goreleaser/goreleaser-action@v3
env:
GORELEASER_CURRENT_TAG: v0.1.29
retention-days: 14
nightly:
needs: build
runs-on: windows-latest
permissions: write-all
if: ${{ github.ref == 'refs/heads/master' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ) }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
version: latest
args: build --skip=validate --clean
- name: Prepare nightly artifacts
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
run: |
Compress-Archive .\target\${{ matrix.target }}\release\*.exe komorebi-nightly-x86_64-pc-windows-msvc.zip
Copy-Item ./target/wix/*.msi -Destination ./komorebi-nightly-x86_64.msi
echo "$((Get-FileHash komorebi-nightly-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-nightly-x86_64-pc-windows-msvc.zip" >checksums.txt
- name: Update nightly
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
shell: bash
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
git tag nightly
kokai release --no-emoji --add-links github:commits,issues --ref nightly >"CHANGELOG.md"
- shell: bash
run: |
gh release delete nightly --yes || true
git push origin :nightly || true
@@ -125,39 +104,100 @@ jobs:
--target $GITHUB_SHA \
--prerelease \
--title "komorebi nightly (${GITHUB_SHA})" \
--notes "This nightly release of komorebi corresponds to [this commit](https://github.com/LGUG2Z/komorebi/commit/${GITHUB_SHA})." \
--notes-file CHANGELOG.md \
komorebi-nightly-x86_64-pc-windows-msvc.zip \
komorebi-nightly-x86_64.msi \
komorebi-nightly-aarch64-pc-windows-msvc.zip \
komorebi-nightly-aarch64.msi \
checksums.txt
# Release
- name: Generate changelog
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
release-dry-run:
needs: build
runs-on: windows-latest
permissions: write-all
if: ${{ github.ref == 'refs/heads/master' }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly
git tag -d nightly || true
kokai release --no-emoji --add-links github:commits,issues --ref "${{ github.ref_name }}" >"CHANGELOG.md"
- uses: softprops/action-gh-release@v2
with:
draft: true
body_path: "CHANGELOG.md"
files: |
checksums.txt
*.zip
*.msi
release:
needs: build
runs-on: windows-latest
permissions: write-all
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.ref_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
if: startsWith(github.ref, 'refs/tags/v')
- uses: softprops/action-gh-release@v2
with:
version: latest
args: release --skip=validate --clean --release-notes=CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
- name: Add MSI to release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: "target/wix/komorebi-*.msi"
body_path: "CHANGELOG.md"
files: |
checksums.txt
*.zip
*.msi
winget:
name: Publish to WinGet
runs-on: ubuntu-latest
needs: build
needs: release
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:

1
.gitignore vendored
View File

@@ -4,4 +4,5 @@
CHANGELOG.md
dummy.go
komorebic/applications.yaml
komorebic/applications.json
/.vs

View File

@@ -1,68 +0,0 @@
# Adapted from https://jondot.medium.com/shipping-rust-binaries-with-goreleaser-d5aa42a46be0
project_name: komorebi
before:
hooks:
- powershell.exe -Command "New-Item -Path . -Name dummy.go -ItemType file -Force"
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'package main'"
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'func main() {}'"
builds:
- id: komorebi
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
- id: komorebic
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
- id: komorebic-no-console
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic-no-console
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
- id: komorebi-gui
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-gui
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
- id: komorebi-bar
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-bar
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-bar.exe" ".\dist\komorebi-bar_windows_amd64_v1\komorebi-bar.exe"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
format: zip
files:
- LICENSE.md
- CHANGELOG.md
checksum:
name_template: checksums.txt
changelog:
sort: asc

2182
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,8 @@ chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.28"
egui_extras = "0.28"
eframe = "0.29"
egui_extras = "0.29"
dirs = "5"
dunce = "1"
hotwatch = "0.5"
@@ -38,17 +38,21 @@ windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.35"
which = "6"
which = "7"
[workspace.dependencies.windows]
version = "0.58"
features = [
"implement",
"Foundation_Numerics",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",

View File

@@ -60,13 +60,17 @@ channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
_komorebi_ development videos. If you would like to be notified of upcoming
videos please subscribe and turn on notifications.
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
showcases the many awesome projects that exist in the _komorebi_ ecosystem.
_komorebi_ is a free and source-available project, and one that encourages you to
make charitable donations if you find the software to be useful and have the
financial means.
I encourage you to make a charitable donation to the [Palestine Children's
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) before you
consider sponsoring me on GitHub.
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or contributing
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
me on GitHub.
[GitHub Sponsors is enabled for this
project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have
@@ -96,7 +100,6 @@ video will answer the majority of your questions.
[![Watch the comparison video](https://img.youtube.com/vi/0LCbS_gm0RA/hqdefault.jpg)](https://www.youtube.com/watch?v=0LCbS_gm0RA)
# Demonstrations
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
@@ -105,7 +108,6 @@ _komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komo
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
11 with a terminal emulator, a web browser and a code editor. The original
video can be viewed
@@ -194,13 +196,19 @@ required.
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
is a fork of the [PolyForm Strict 1.0.0
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
this means that you are free to do whatever you want with `komorebi` other than
redistribution, or distribution of new works (ie. hard-forks) based on the
software.
this means that you are free to do whatever you want with `komorebi` for
personal use other than redistribution, or distribution of new works (ie.
hard-forks) based on the software.
Anyone is free to make their own fork of `komorebi` with changes intended
either for personal use or for integration back upstream via pull requests.
The [Komorebi 1.0.0 License](./LICENSE.md) does not permit any kind of
commercial use.
A dedicated license and EULA will be introduced in 2025 for both commercial and
noncommercial organizations.
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
code contributions to `komorebi` are licensed.
@@ -359,7 +367,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.29"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.30"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -10,9 +10,8 @@ Options:
Desired ease function for animation
[default: linear]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo,
ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
-h, --help
Print help

View File

@@ -1,7 +1,7 @@
# application-specific-configuration-schema
```
Generate a JSON Schema for applications.yaml
Generate a JSON Schema for applications.json
Usage: komorebic.exe application-specific-configuration-schema

View File

@@ -18,7 +18,7 @@ Arguments:
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle, unfocused]
[possible values: single, stack, monocle, unfocused, floating]
-h, --help
Print help

View File

@@ -0,0 +1,16 @@
# convert-app-specific-configuration
```
Convert a v1 ASC YAML file to a v2 ASC JSON file
Usage: komorebic.exe convert-app-specific-configuration <PATH>
Arguments:
<PATH>
YAML file from which the application-specific configurations should be loaded
Options:
-h, --help
Print help
```

View File

@@ -9,9 +9,6 @@ Options:
-c, --config <CONFIG>
Path to a static configuration JSON file
-f, --ffm
Enable komorebi's custom focus-follows-mouse implementation
--whkd
Enable autostart of whkd

View File

@@ -1,7 +1,7 @@
# fetch-app-specific-configuration
```
Fetch the latest version of applications.yaml from komorebi-application-specific-configuration
Fetch the latest version of applications.json from komorebi-application-specific-configuration
Usage: komorebic.exe fetch-app-specific-configuration

View File

@@ -1,23 +0,0 @@
# focus-follows-mouse
```
Enable or disable focus follows mouse for the operating system
Usage: komorebic.exe focus-follows-mouse [OPTIONS] <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-i, --implementation <IMPLEMENTATION>
[default: windows]
Possible values:
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
- windows: The native (legacy) Windows FFM implementation
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -1,16 +0,0 @@
# format-app-specific-configuration
```
Format a YAML file for use with the 'ahk-app-specific-configuration' command
Usage: komorebic.exe format-app-specific-configuration <PATH>
Arguments:
<PATH>
YAML file from which the application-specific configurations should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,9 +1,9 @@
# float-rule
# ignore-rule
```
Add a rule to always float the specified application
Add a rule to ignore the specified application
Usage: komorebic.exe float-rule <IDENTIFIER> <ID>
Usage: komorebic.exe ignore-rule <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>

View File

@@ -1,16 +0,0 @@
# load-custom-layout
```
Load a custom layout from file for the focused workspace
Usage: komorebic.exe load-custom-layout <PATH>
Arguments:
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,22 +0,0 @@
# named-workspace-custom-layout-rule
```
Add a dynamic custom layout for the specified workspace
Usage: komorebic.exe named-workspace-custom-layout-rule <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
Arguments:
<WORKSPACE>
Target workspace name
<AT_CONTAINER_COUNT>
The number of window containers on-screen required to trigger this layout rule
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,19 +0,0 @@
# named-workspace-custom-layout
```
Set a custom layout for the specified workspace
Usage: komorebic.exe named-workspace-custom-layout <WORKSPACE> <PATH>
Arguments:
<WORKSPACE>
Target workspace name
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -6,9 +6,6 @@ Start komorebi.exe as a background process
Usage: komorebic.exe start [OPTIONS]
Options:
-f, --ffm
Allow the use of komorebi's custom focus-follows-mouse implementation
-c, --config <CONFIG>
Path to a static configuration JSON file

View File

@@ -9,6 +9,9 @@ Options:
--whkd
Stop whkd if it is running as a background process
--ahk
Stop ahk if it is running as a background process
--bar
Stop komorebi-bar if it is running as a background process

View File

@@ -0,0 +1,12 @@
# toggle-float-override
```
Enable or disable float override, which makes it so every new window opens in floating mode
Usage: komorebic.exe toggle-float-override
Options:
-h, --help
Print help
```

View File

@@ -1,19 +0,0 @@
# toggle-focus-follows-mouse
```
Toggle focus follows mouse for the operating system
Usage: komorebic.exe toggle-focus-follows-mouse [OPTIONS]
Options:
-i, --implementation <IMPLEMENTATION>
[default: windows]
Possible values:
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
- windows: The native (legacy) Windows FFM implementation
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -0,0 +1,12 @@
# toggle-workspace-float-override
```
Enable or disable float override, which makes it so every new window opens in floating 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
Usage: komorebic.exe toggle-workspace-float-override
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# toggle-workspace-window-container-behaviour
```
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the global value
Usage: komorebic.exe toggle-workspace-window-container-behaviour
Options:
-h, --help
Print help
```

View File

@@ -1,25 +0,0 @@
# workspace-custom-layout-rule
```
Add a dynamic custom layout for the specified workspace
Usage: komorebic.exe workspace-custom-layout-rule <MONITOR> <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index on the specified monitor (zero-indexed)
<AT_CONTAINER_COUNT>
The number of window containers on-screen required to trigger this layout rule
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,22 +0,0 @@
# workspace-custom-layout
```
Set a custom layout for the specified workspace
Usage: komorebic.exe workspace-custom-layout <MONITOR> <WORKSPACE> <PATH>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index on the specified monitor (zero-indexed)
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,29 @@
# Autostart
If you would like to autostart `komorebi`, you can use the `komorebic enable-autostart` command to generate a shortcut
in the `shell:startup` folder.
```
Generates the komorebi.lnk shortcut in shell:startup to autostart komorebi
Usage: komorebic.exe enable-autostart [OPTIONS]
Options:
-c, --config <CONFIG>
Path to a static configuration JSON file
-f, --ffm
Enable komorebi's custom focus-follows-mouse implementation
--whkd
Enable autostart of whkd
--ahk
Enable autostart of ahk
--bar
Enable autostart of komorebi-bar
-h, --help
Print help
```

View File

@@ -1,70 +0,0 @@
# Custom Layouts
Particularly for users of ultrawide monitors, traditional tiling layouts may
not seem like the most efficient use of screen space. If you feel this is the
case with any of the default layouts, you are also welcome to create your own
custom layouts and save them as JSON or YAML.
If you're not comfortable writing the layouts directly in JSON or YAML, you can
use the [komorebi Custom Layout
Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to
interactively define a custom layout, and then copy the generated JSON content.
Custom layouts can be loaded on the current workspace or configured for a
specific workspace in the `komorebi.json` configuration file.
```json
{
"monitors": [
{
"workspaces": [
{
"name": "personal",
"custom_layout": "C:/Users/LGUG2Z/my-custom-layout.json"
},
]
}
]
}
```
The fundamental building block of a custom _komorebi_ layout is the Column.
Columns come in three variants:
- **Primary**: This is where your primary focus will be on the screen most of
the time. There must be exactly one Primary Column in any custom layout.
Optionally, you can specify the percentage of the screen width that you want
the Primary Column to occupy.
- **Secondary**: This is an optional column that can either be full height of
split horizontally into a fixed number of maximum rows. There can be any
number of Secondary Columns in a custom layout.
- **Tertiary**: This is the final column where any remaining windows will be
split horizontally into rows as they get added.
If there is only one window on the screen when a custom layout is selected,
that window will take up the full work area of the screen.
If the number of windows is equal to or less than the total number of columns
defined in a custom layout, the windows will be arranged in an equal-width
columns.
When the number of windows is greater than the number of columns defined in the
custom layout, the windows will begin to be arranged according to the
constraints set on the Primary and Secondary columns of the layout.
Here is an example custom layout that can be used as a starting point for your
own:
```yaml
- column: Secondary
configuration: !Horizontal 2 # max number of rows
- column: Primary
configuration: !WidthPercentage 50 # percentage of screen
- column: Tertiary
configuration: Horizontal
```
<!-- TODO: Record a new video -->
[![Watch the tutorial video](https://img.youtube.com/vi/SgmBHKEOcQ4/hqdefault.jpg)](https://www.youtube.com/watch?v=SgmBHKEOcQ4)

View File

@@ -0,0 +1,16 @@
# Floating Windows
Sometimes you will want a specific application to be managed as a floating window.
You can add rules to enforce this behaviour in the `komorebi.json` configuration file.
```json
{
"floating_applications": [
{
"kind": "Title",
"id": "Media Player",
"matching_strategy": "Equals"
}
]
}
```

View File

@@ -1,34 +0,0 @@
# Focus Follows Mouse
`komorebi` supports two focus-follows-mouse implementations; the native Windows
Xmouse implementation, which treats the desktop, the task bar, and the system
tray as windows and switches focus to them eagerly, and a custom `komorebi`
implementation, which only considers windows managed by `komorebi` as valid
targets to switch focus to when moving the mouse.
To enable the `komorebi` implementation you must start the process with the
`--ffm` flag to explicitly enable the feature. This is because the mouse
tracking required for this feature significantly increases the CPU usage of the
process (on my machine, it jumps from <1% to ~4~), and this CPU increase
persists regardless of whether focus-follows-mouse is enabled or disabled at
any given time via `komorebic`'s configuration commands.
If the `komorebi` process has been started with the `--ffm` flag, you can
enable focus follows mouse behaviour in the `komorebi.json` configuration file.
```json
{
"focus_follows_mouse": "Komorebi"
}
```
When calling any of the `komorebic` commands related to focus-follows-mouse
functionality, the `windows` implementation will be chosen as the default
implementation. You can optionally specify the `komorebi` implementation by
passing it as an argument to the `--implementation` flag:
```powershell
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
```

View File

@@ -5,12 +5,12 @@ applications are [already generated for
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
Sometimes you will want a specific application to never be tiled, and instead
float all the time. You can add rules to enforce this behaviour in the
be completely ignored. You can add rules to enforce this behaviour in the
`komorebi.json` configuration file.
```json
{
"float_rules": [
"ignore_rules": [
{
"kind": "Title",
"id": "Media Player",

View File

@@ -0,0 +1,19 @@
# Multiple Bar Instances
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
by maintaining multiple `komorebi.bar.json` configuration files and specifying their paths in the `bar_configurations`
array in your `komorebi.json` configuration file.
```json
{
"bar_configurations": [
"C:/Users/LGUG2Z/komorebi.bar.monitor1.json",
"C:/Users/LGUG2Z/komorebi.bar.monitor2.json"
]
}
```
You may also use `$Env:USERPROFILE` or `$Env:KOMOREBI_CONFIG_HOME` when specifying the paths.
The main difference between different `komorebi.bar.json` files will be the value of `monitor.index` which is used to
target the monitor for each instance of `komorebi-bar`.

View File

@@ -62,7 +62,7 @@ using `default_workspace_padding` and `default_container_padding`.
You may have seen videos and screenshots of people using `komorebi` with a
thick, colourful active window border. You can also enable this by setting
`active_window_border` to `true`. However, please be warned that this feature
`border` to `true`. However, please be warned that this feature
is a crude hack trying to compensate for the insistence of Microsoft Windows
design teams to make custom borders with widths that are actually visible to
the user a thing of the past and removing this capability from the Win32 API.
@@ -162,6 +162,8 @@ If you have an ultrawide monitor, I recommend using this layout.
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
The `grid` layout does not support resizing windows tiles.
```
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
| | | | | | | | | | | | | | |
@@ -177,7 +179,7 @@ If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layou
`whkd` is a fairly basic piece of software with a simple configuration format:
key bindings go to the left of the colon, and shell commands go to the right of the
colon. By default, the `whkdrc` file should be located in the `$Env:USERPROFILE/.config/` directory.
colon.
Please remember that `whkd` does not support overriding Microsoft's limitations
on hotkey bindings that include the `Windows` key. If this is important to you,
@@ -188,6 +190,25 @@ bindings for `komorebic` commands instead.
{% include "./whkdrc.sample" %}
```
### Configuration
`whkd` searches for a `whkdrc` configuration file in the following locations:
* `$Env:WHKD_CONFIG_HOME`
* `$Env:USERPROFILE/.config`
It is also possible to change a hotkey behavior depending on which application has focus:
```
alt + n [
# ProcessName as shown by `Get-Process`
Firefox : echo "hello firefox"
# Spaces are fine, no quotes required
Google Chrome : echo "hello chrome"
]
```
### Setting .shell
There is one special directive at the top of the file, `.shell` which can be

View File

@@ -134,6 +134,26 @@ an offline machine to install.
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
first-time set up and running komorebi require an internet connection).
## Upgrades
Before upgrading, make sure to run `komorebic stop --whkd --bar`. This is to ensure that all the current
komorebi-related exe files can be replaced without issue.
Then, depending on whether you installed via `scoop` or `winget`, you can run the appropriate command:
```powershell
# for winget
winget upgrade LGUG2Z.komorebi
```
```powershell
# for scoop
scoop update komorebi
```
Once the upgrade is completed you can confirm that you have the latest version by running `komorebic --version`, and
then start it with `komorebic start --whkd --bar`.
## Uninstallation
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
@@ -147,7 +167,7 @@ files created by the `quickstart` command and any other runtime files:
```powershell
rm $Env:USERPROFILE\komorebi.json
rm $Env:USERPROFILE\applications.yaml
rm $Env:USERPROFILE\applications.json
rm $Env:USERPROFILE\.config\whkdrc
rm -r -Force $Env:LOCALAPPDATA\komorebi
```

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
"default_workspace_padding": 20,

View File

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

View File

@@ -1,5 +1,15 @@
# Troubleshooting
## Phantom Tiles
Sometimes you may experience an application which leaves "ghost tiles" on a workspace, where there is space reserved for
a window but no window visible.
You can ignore these windows by following these steps:
* Run `komorebic visible-windows` to find details about the invisible window
* Using that information, [create a rule to ignore that window](common-workflows/ignore-windows.md)
## AutoHotKey executable not found
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
@@ -85,10 +95,10 @@ running `komorebic stop` and `komorebic start`.
To avoid waiting an eternity:
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
Settings_
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
Settings_
_Turn off the display: 1 minute_
_Turn off the display: 1 minute_
Allow a minute for the display to reset, then once it actually shuts off
allow for any additional time as prompted by your monitor for the cycle to

View File

@@ -7,36 +7,36 @@ clean:
fmt:
cargo +nightly fmt
cargo +stable clippy
prettier --write README.md
prettier --write .goreleaser.yml
prettier --write .github/ISSUE_TEMPLATE/bug_report.yml
prettier --write .github/ISSUE_TEMPLATE/config.yml
prettier --write .github/ISSUE_TEMPLATE/feature_request.yml
prettier --write .github/dependabot.yml
prettier --write .github/FUNDING.yml
prettier --write .github/workflows/windows.yaml
install-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
install-target target:
cargo +stable install --path {{ target }} --locked
install:
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi-gui
just install-target komorebi-bar
just install-target komorebi
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
run:
cargo +stable run --bin komorebi --locked
run target:
cargo +stable run --bin {{ target }} --locked
warn $RUST_LOG="warn":
just run
warn target $RUST_LOG="warn":
just run {{ target }}
info $RUST_LOG="info":
just run
info target $RUST_LOG="info":
just run {{ target }}
debug $RUST_LOG="debug":
just run
debug target $RUST_LOG="debug":
just run {{ target }}
trace $RUST_LOG="trace":
just run
trace target $RUST_LOG="trace":
just run {{ target }}
deadlock $RUST_LOG="trace":
cargo +stable run --bin komorebi --locked --features deadlock_detection

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.30"
version = "0.1.31"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -9,7 +9,6 @@ edition = "2021"
komorebi-client = { path = "../komorebi-client" }
komorebi-themes = { path = "../komorebi-themes" }
atomic_float = "1"
chrono = { workspace = true }
clap = { workspace = true }
color-eyre = { workspace = true }
@@ -17,22 +16,21 @@ crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = "0.6.0"
egui-phosphor = "0.7"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.31"
num = "0.4.3"
num-derive = "0.4.2"
num-traits = "0.2.19"
random_word = { version = "0.4.3", features = ["en"] }
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }

View File

@@ -1,13 +1,25 @@
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::process_hwnd;
use crate::render::Color32Ext;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::DPI;
use crate::BAR_HEIGHT;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crossbeam_channel::Receiver;
use eframe::egui::Align;
use eframe::egui::Align2;
use eframe::egui::Area;
use eframe::egui::CentralPanel;
use eframe::egui::Color32;
use eframe::egui::Context;
@@ -16,13 +28,13 @@ use eframe::egui::FontDefinitions;
use eframe::egui::FontFamily;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Id;
use eframe::egui::Layout;
use eframe::egui::Margin;
use eframe::egui::Pos2;
use eframe::egui::Rgba;
use eframe::egui::Style;
use eframe::egui::TextStyle;
use eframe::egui::Vec2;
use eframe::egui::ViewportCommand;
use eframe::egui::Visuals;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
@@ -38,8 +50,10 @@ use std::sync::Arc;
pub struct Komobar {
pub config: Arc<KomobarConfig>,
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub center_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_config: Receiver<KomobarConfig>,
@@ -146,16 +160,47 @@ impl Komobar {
Self::add_custom_font(ctx, font_family);
}
if let Some(viewport) = &config.viewport {
let dpi = DPI.load(Ordering::SeqCst);
if let Some(position) = viewport.position {
let pos2 = Pos2::new(position.x / dpi, position.y / dpi);
ctx.send_viewport_cmd(ViewportCommand::OuterPosition(pos2));
let position = 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,
}),
end: Some(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
}),
});
if let Some(hwnd) = process_hwnd() {
let start = position.start.unwrap_or(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
});
let end = position.end.unwrap_or(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
});
if end.y == 0.0 {
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
}
if let Some(position) = viewport.inner_size {
let vec2 = Vec2::new(position.x / dpi, position.y / dpi);
ctx.send_viewport_cmd(ViewportCommand::InnerSize(vec2));
let rect = komorebi_client::Rect {
left: start.x as i32,
top: start.y as i32,
right: end.x as i32,
bottom: end.y as i32,
};
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
@@ -207,6 +252,30 @@ impl Komobar {
}
}
// apply rounding to the widgets
if let Some(
Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config),
) = &config.grouping
{
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.rounding = rounding.into();
style.visuals.widgets.inactive.rounding = rounding.into();
style.visuals.widgets.hovered.rounding = rounding.into();
style.visuals.widgets.active.rounding = rounding.into();
style.visuals.widgets.open.rounding = rounding.into();
});
}
}
let theme_color = *self.bg_color.borrow();
self.render_config
.replace(config.new_renderconfig(theme_color));
self.bg_color
.replace(theme_color.try_apply_alpha(config.transparency_alpha));
if let Some(font_size) = &config.font_size {
tracing::info!("attempting to set custom font size: {font_size}");
Self::set_font_size(ctx, *font_size);
@@ -221,7 +290,17 @@ impl Komobar {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Left);
side = Some(Alignment::Left);
}
}
if let Some(center_widgets) = &config.center_widgets {
for (idx, widget_config) in center_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Alignment::Center);
}
}
}
@@ -229,7 +308,7 @@ impl Komobar {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Right);
side = Some(Alignment::Right);
}
}
@@ -239,6 +318,14 @@ impl Komobar {
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut center_widgets = match &config.center_widgets {
Some(center_widgets) => center_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>(),
None => vec![],
};
let mut right_widgets = config
.right_widgets
.iter()
@@ -263,20 +350,23 @@ impl Komobar {
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Side::Left => left_widgets[idx] = boxed,
Side::Right => right_widgets[idx] = boxed,
Alignment::Left => left_widgets[idx] = boxed,
Alignment::Center => center_widgets[idx] = boxed,
Alignment::Right => right_widgets[idx] = boxed,
}
}
right_widgets.reverse();
self.left_widgets = left_widgets;
self.center_widgets = center_widgets;
self.right_widgets = right_widgets;
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
}
pub fn new(
cc: &eframe::CreationContext<'_>,
rx_gui: Receiver<komorebi_client::Notification>,
@@ -285,8 +375,10 @@ impl Komobar {
) -> Self {
let mut komobar = Self {
config: config.clone(),
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
left_widgets: vec![],
center_widgets: vec![],
right_widgets: vec![],
rx_gui,
rx_config,
@@ -294,6 +386,8 @@ impl Komobar {
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
};
komobar.apply_config(&cc.egui_ctx, &config, None);
// needs a double apply the first time for some reason
komobar.apply_config(&cc.egui_ctx, &config, None);
komobar
@@ -353,13 +447,10 @@ impl Komobar {
}
}
impl eframe::App for Komobar {
// TODO: I think this is needed for transparency??
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
// egui::Rgba::TRANSPARENT.to_array()
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
// background[3] = 0.9;
// background
// }
// Needed for transparency
fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
Rgba::TRANSPARENT.to_array()
}
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
@@ -401,17 +492,54 @@ impl eframe::App for Komobar {
Frame::none().fill(*self.bg_color.borrow())
};
CentralPanel::default().frame(frame).show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui);
}
});
let mut render_config = self.render_config.borrow_mut();
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui);
CentralPanel::default().frame(frame).show(ctx, |ui| {
// Apply grouping logic for the bar as a whole
render_config.clone().apply_on_bar(ui, |ui| {
ui.horizontal_centered(|ui| {
// Left-aligned widgets layout
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
let mut render_conf = *render_config;
render_conf.alignment = Some(Alignment::Left);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
// Right-aligned widgets layout
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let mut render_conf = *render_config;
render_conf.alignment = Some(Alignment::Right);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
if !self.center_widgets.is_empty() {
// Floating center widgets
Area::new(Id::new("center_panel"))
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
.show(ctx, |ui| {
Frame::none().show(ui, |ui| {
ui.horizontal_centered(|ui| {
let mut render_conf = *render_config;
render_conf.alignment = Some(Alignment::Center);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.center_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
});
});
}
})
})
@@ -420,7 +548,8 @@ impl eframe::App for Komobar {
}
#[derive(Copy, Clone)]
enum Side {
pub enum Alignment {
Left,
Center,
Right,
}

View File

@@ -1,5 +1,6 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -23,6 +24,8 @@ pub struct BatteryConfig {
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<BatteryConfig> for Battery {
@@ -30,6 +33,7 @@ impl From<BatteryConfig> for Battery {
let manager = Manager::new().unwrap();
let mut last_state = String::new();
let mut state = None;
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(mut batteries) = manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
@@ -40,7 +44,12 @@ impl From<BatteryConfig> for Battery {
_ => {}
}
last_state = format!("{percentage}%");
last_state = match prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
@@ -49,6 +58,7 @@ impl From<BatteryConfig> for Battery {
manager,
last_state,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
state: state.unwrap_or(BatteryState::Discharging),
last_updated: Instant::now(),
}
@@ -65,6 +75,7 @@ pub struct Battery {
manager: Manager,
pub state: BatteryState,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_state: String,
last_updated: Instant,
}
@@ -86,7 +97,12 @@ impl Battery {
_ => {}
}
output = format!("{percentage:.0}%");
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
@@ -99,7 +115,7 @@ impl Battery {
}
impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
@@ -116,7 +132,10 @@ impl BarWidget for Battery {
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
emoji.to_string(),
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
@@ -128,14 +147,14 @@ impl BarWidget for Battery {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
config.apply_on_widget(true, ui, |ui| {
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
});
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,3 +1,4 @@
use crate::render::Grouping;
use crate::widget::WidgetConfig;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
@@ -7,14 +8,16 @@ use komorebi_client::Rect;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
/// The `komorebi.bar.json` configuration file reference for `v0.1.31`
pub struct KomobarConfig {
/// Viewport options (see: https://docs.rs/egui/latest/egui/viewport/struct.ViewportBuilder.html)
pub viewport: Option<ViewportConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// Monitor options
pub monitor: MonitorConfig,
@@ -26,18 +29,57 @@ pub struct KomobarConfig {
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Alpha value for the color transparency [[0-255]] (default: 200)
pub transparency_alpha: Option<u8>,
/// Spacing between widgets (default: 10.0)
pub widget_spacing: Option<f32>,
/// Visual grouping for widgets
pub grouping: Option<Grouping>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right)
pub center_widgets: Option<Vec<WidgetConfig>>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
impl KomobarConfig {
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("position", ["viewport"]);
map.insert("end", ["inner_frame"]);
let mut display = false;
for aliases in map.values() {
for a in aliases {
if raw.contains(a) {
display = true;
}
}
}
if display {
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
println!(r#""{alias}" is now "{canonical}""#);
}
}
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ViewportConfig {
pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
pub position: Option<Position>,
#[serde(alias = "position")]
pub start: Option<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
pub inner_size: Option<Position>,
#[serde(alias = "inner_size")]
pub end: Option<Position>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -103,11 +145,13 @@ impl From<Position> for Pos2 {
pub enum KomobarTheme {
/// A theme from catppuccin-egui
Catppuccin {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin,
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: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
},
@@ -131,3 +175,25 @@ impl From<KomorebiTheme> for KomobarTheme {
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum LabelPrefix {
/// Show no prefix
None,
/// Show an icon
Icon,
/// Show text
Text,
/// Show an icon and text
IconAndText,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum DisplayFormat {
/// Show only icon
Icon,
/// Show only text
Text,
/// Show both icon and text
IconAndText,
}

121
komorebi-bar/src/cpu.rs Normal file
View File

@@ -0,0 +1,121 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CpuConfig {
/// Enable the Cpu widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<CpuConfig> for Cpu {
fn from(value: CpuConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
system.refresh_cpu_usage();
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Cpu {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Cpu {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage();
self.last_updated = now;
}
let used = self.system.global_cpu_usage();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
}
}
}
impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CPU.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
});
}
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -19,6 +20,8 @@ pub struct DateConfig {
pub enable: bool,
/// Set the Date format
pub format: DateFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<DateConfig> for Date {
@@ -26,6 +29,7 @@ impl From<DateConfig> for Date {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
@@ -70,6 +74,7 @@ impl DateFormat {
pub struct Date {
pub enable: bool,
pub format: DateFormat,
label_prefix: LabelPrefix,
}
impl Date {
@@ -81,9 +86,9 @@ impl Date {
}
impl BarWidget for Date {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
@@ -93,31 +98,40 @@ impl BarWidget for Date {
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::CALENDAR_DOTS.to_string(),
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CALENDAR_DOTS.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "DATE: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.next()
}
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.next()
}
});
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,36 +1,44 @@
use crate::bar::apply_theme;
use crate::config::DisplayFormat;
use crate::config::KomobarTheme;
use crate::komorebi_layout::KomorebiLayout;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use crate::MONITOR_INDEX;
use crossbeam_channel::Receiver;
use eframe::egui::text::LayoutJob;
use crossbeam_channel::TryRecvError;
use eframe::egui::vec2;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::SelectableLabel;
use eframe::egui::Margin;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextStyle;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use image::RgbaImage;
use komorebi_client::CycleDirection;
use komorebi_client::Container;
use komorebi_client::NotificationEvent;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_client::Window;
use komorebi_client::Workspace;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
@@ -53,20 +61,28 @@ pub struct KomorebiWorkspacesConfig {
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
/// Display format of the workspace
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
/// List of layout options
pub options: Option<Vec<KomorebiLayout>>,
/// Display format of the current layout
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
pub enable: bool,
/// Show the icon of the currently focused window
pub show_icon: bool,
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
pub show_icon: Option<bool>,
/// Display format of the currently focused window
pub display: Option<DisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -83,14 +99,6 @@ impl From<&KomorebiConfig> for Komorebi {
if let Some(configuration_switcher) = &value.configuration_switcher {
let mut configuration_switcher = configuration_switcher.clone();
for (_, location) in configuration_switcher.configurations.iter_mut() {
if let Ok(expanded) = std::env::var("KOMOREBI_CONFIG_HOME") {
*location = location.replace("$Env:KOMOREBI_CONFIG_HOME", &expanded);
}
if let Ok(expanded) = std::env::var("USERPROFILE") {
*location = location.replace("$Env:USERPROFILE", &expanded);
}
*location = dunce::simplified(&PathBuf::from(location.clone()))
.to_string_lossy()
.to_string();
@@ -108,11 +116,12 @@ impl From<&KomorebiConfig> for Komorebi {
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: (vec![], vec![], 0),
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
stack_accent: None,
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
})),
workspaces: value.workspaces,
layout: value.layout,
layout: value.layout.clone(),
focused_window: value.focused_window,
configuration_switcher,
}
@@ -129,233 +138,89 @@ pub struct Komorebi {
}
impl BarWidget for Komorebi {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
if self.workspaces.enable {
let mut update = None;
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
if ui
.add(SelectableLabel::new(
komorebi_notification_state.selected_workspace.eq(ws),
ws.to_string(),
))
.clicked()
{
update = Some(ws.to_string());
let mut proceed = true;
if !komorebi_notification_state.workspaces.is_empty() {
let format = self.workspaces.display.unwrap_or(DisplayFormat::Text);
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
.is_err()
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, container_information)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if SelectableFrame::new(
komorebi_notification_state.selected_workspace.eq(ws),
)
.show(ui, |ui| {
let mut has_icon = false;
if proceed
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
.is_err()
{
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
proceed = false;
}
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
let icons: Vec<_> =
container_information.icons.iter().flatten().collect();
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if proceed && komorebi_client::send_message(&SocketMessage::Retile).is_err() {
tracing::error!("could not send message to komorebi: Retile");
}
}
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
ui.add_space(WIDGET_SPACING);
}
if let Some(layout) = self.layout {
if layout.enable {
if ui
.add(
Label::new(komorebi_notification_state.layout.to_string())
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
match komorebi_notification_state.layout {
KomorebiLayout::Default(_) => {
if komorebi_client::send_message(&SocketMessage::CycleLayout(
CycleDirection::Next,
))
.is_err()
{
tracing::error!("could not send message to komorebi: CycleLayout");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err()
{
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
ui.add_space(WIDGET_SPACING);
}
}
if let Some(configuration_switcher) = &self.configuration_switcher {
if configuration_switcher.enable {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file()
&& ui
.add(Label::new(name).selectable(false).sense(Sense::click()))
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
canonicalized,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
proceed = false;
}
if let Some(rect) = komorebi_notification_state.work_area_offset {
if proceed {
match komorebi_client::send_query(&SocketMessage::Query(
komorebi_client::StateQuery::FocusedMonitorIndex,
)) {
Ok(idx) => {
if let Ok(monitor_idx) = idx.parse::<usize>() {
if komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
rect,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MonitorWorkAreaOffset"
if !icons.is_empty() {
Frame::none()
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y,
))
.show(ui, |ui| {
for icon in icons {
ui.add(
Image::from(&img_to_texture(ctx, icon))
.maintain_aspect_ratio(true)
.shrink_to_fit(),
);
if !has_icon {
has_icon = true;
}
}
}
}
Err(_) => {
tracing::error!(
"could not send message to komorebi: Query"
);
}
});
}
}
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
// draw a custom icon when there is no app icon
if match format {
DisplayFormat::Icon => !has_icon,
_ => false,
} {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
let titles = &komorebi_notification_state.focused_container_information.0;
let icons = &komorebi_notification_state.focused_container_information.1;
let focused_window_idx =
komorebi_notification_state.focused_container_information.2;
let (response, painter) =
ui.allocate_painter(Vec2::splat(font_id.size), Sense::hover());
let stroke =
Stroke::new(1.0, ctx.style().visuals.selection.stroke.color);
let mut rect = response.rect;
let rounding = Rounding::same(rect.width() * 0.1);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke);
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
let iter = titles.iter().zip(icons.iter());
for (i, (title, icon)) in iter.enumerate() {
if focused_window.show_icon {
if let Some(img) = icon {
ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.max_height(15.0),
);
}
}
if i == focused_window_idx {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let layout_job = LayoutJob::simple(
title.to_string(),
font_id.clone(),
komorebi_notification_state
.stack_accent
.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
if titles.len() > 1 {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
);
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title).selectable(false).truncate(),
);
}
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
response.on_hover_text(ws.to_string())
} else if match format {
DisplayFormat::Icon => has_icon,
_ => false,
} {
ui.response().on_hover_text(ws.to_string())
} else {
ui.add(Label::new(ws.to_string()).selectable(false))
}
})
.clicked()
{
update = Some(ws.to_string());
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
@@ -364,33 +229,235 @@ impl BarWidget for Komorebi {
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
if proceed
&& komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
komorebi_notification_state.monitor_index,
i,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
"could not send message to komorebi: FocusWorkspaceNumber"
);
proceed = false;
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
if proceed
&& komorebi_client::send_message(
&SocketMessage::RetileWithResizeDimensions,
)
.is_err()
{
tracing::error!("could not send message to komorebi: Retile");
}
}
}
ui.add_space(WIDGET_SPACING);
}
});
}
ui.add_space(WIDGET_SPACING);
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
}
if let Some(layout_config) = &self.layout {
if layout_config.enable {
let workspace_idx: Option<usize> = komorebi_notification_state
.workspaces
.iter()
.position(|o| komorebi_notification_state.selected_workspace.eq(&o.0));
komorebi_notification_state.layout.show(
ctx,
ui,
config,
layout_config,
workspace_idx,
);
}
}
if let Some(configuration_switcher) = &self.configuration_switcher {
if configuration_switcher.enable {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file() {
config.apply_on_widget(false, ui,|ui|{
if SelectableFrame::new(false).show(ui, |ui|{
ui.add(Label::new(name).selectable(false))
})
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
canonicalized,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
proceed = false;
}
if let Some(rect) = komorebi_notification_state.work_area_offset {
if proceed {
match komorebi_client::send_query(&SocketMessage::Query(
komorebi_client::StateQuery::FocusedMonitorIndex,
)) {
Ok(idx) => {
if let Ok(monitor_idx) = idx.parse::<usize>() {
if komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
rect,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MonitorWorkAreaOffset"
);
}
}
}
Err(_) => {
tracing::error!(
"could not send message to komorebi: Query"
);
}
}
}
}
}});
}
}
}
}
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
let titles = &komorebi_notification_state
.focused_container_information
.titles;
if !titles.is_empty() {
config.apply_on_widget(false, ui, |ui| {
let icons = &komorebi_notification_state
.focused_container_information
.icons;
let focused_window_idx = komorebi_notification_state
.focused_container_information
.focused_window_idx;
let iter = titles.iter().zip(icons.iter());
for (i, (title, icon)) in iter.enumerate() {
let selected = i == focused_window_idx;
if SelectableFrame::new(selected)
.show(ui, |ui| {
// handle legacy setting
let format = focused_window.display.unwrap_or(
if focused_window.show_icon.unwrap_or(false) {
DisplayFormat::IconAndText
} else {
DisplayFormat::Text
},
);
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format
{
if let Some(img) = icon {
Frame::none()
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y,
))
.show(ui, |ui| {
let response = ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.shrink_to_fit(),
);
if let DisplayFormat::Icon = format {
response.on_hover_text(title);
}
});
}
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format
{
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title).selectable(false).truncate(),
);
}
})
.clicked()
{
if selected {
return;
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(
i,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
);
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
}
}
});
}
}
}
}
}
@@ -404,33 +471,15 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<String>,
pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>,
pub selected_workspace: String,
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
pub focused_container_information: KomorebiNotificationStateContainerInformation,
pub layout: KomorebiLayout,
pub hide_empty_workspaces: bool,
pub mouse_follows_focus: bool,
pub work_area_offset: Option<Rect>,
pub stack_accent: Option<Color32>,
}
#[derive(Copy, Clone, Debug)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Floating,
Paused,
Custom,
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
pub monitor_index: usize,
}
impl KomorebiNotificationState {
@@ -445,94 +494,152 @@ impl KomorebiNotificationState {
rx_gui: Receiver<komorebi_client::Notification>,
bg_color: Rc<RefCell<Color32>>,
) {
if let Ok(notification) = rx_gui.try_recv() {
if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) =
notification.event
{
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
tracing::info!("applied theme from updated komorebi.json");
match rx_gui.try_recv() {
Err(error) => match error {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
tracing::error!(
"failed to receive komorebi notification on gui thread: {error}"
);
}
},
Ok(notification) => {
match notification.event {
NotificationEvent::WindowManager(_) => {}
NotificationEvent::Socket(message) => match message {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), bg_color.clone());
tracing::info!("applied theme from updated komorebi.json");
}
}
}
SocketMessage::Theme(theme) => {
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
},
}
self.monitor_index = monitor_index;
self.mouse_follows_focus = notification.state.mouse_follows_focus;
let monitor = &notification.state.monitors.elements()[monitor_index];
self.work_area_offset =
notification.state.monitors.elements()[monitor_index].work_area_offset();
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_show = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
} else {
true
};
if should_show {
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
ws.into(),
));
}
}
}
self.mouse_follows_focus = notification.state.mouse_follows_focus;
self.workspaces = workspaces;
let monitor = &notification.state.monitors.elements()[monitor_index];
self.work_area_offset =
notification.state.monitors.elements()[monitor_index].work_area_offset();
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_add = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
if monitor.workspaces()[focused_workspace_idx]
.monocle_container()
.is_some()
{
self.layout = KomorebiLayout::Monocle;
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
} else if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
} else {
true
};
if should_add {
workspaces.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => {
KomorebiLayout::Default(*layout)
}
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
}
}
self.workspaces = workspaces;
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
}
if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
}
if let Some(container) = monitor.workspaces()[focused_workspace_idx].monocle_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else if let Some(container) =
monitor.workspaces()[focused_workspace_idx].focused_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else {
self.focused_container_information.0.clear();
self.focused_container_information.1.clear();
self.focused_container_information.2 = 0;
self.focused_container_information =
(&monitor.workspaces()[focused_workspace_idx]).into();
}
}
}
}
#[derive(Clone, Debug)]
pub struct KomorebiNotificationStateContainerInformation {
pub titles: Vec<String>,
pub icons: Vec<Option<RgbaImage>>,
pub focused_window_idx: usize,
}
impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
fn from(value: &Workspace) -> Self {
let mut container_info = Self::EMPTY;
if let Some(container) = value.monocle_container() {
container_info = container.into();
} else if let Some(container) = value.focused_container() {
container_info = container.into();
}
for floating_window in value.floating_windows() {
if floating_window.is_focused() {
container_info = floating_window.into();
}
}
container_info
}
}
impl From<&Container> for KomorebiNotificationStateContainerInformation {
fn from(value: &Container) -> Self {
Self {
titles: value
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
icons: value
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
focused_window_idx: value.focused_window_idx(),
}
}
}
impl From<&Window> for KomorebiNotificationStateContainerInformation {
fn from(value: &Window) -> Self {
Self {
titles: vec![value.title().unwrap_or_default()],
icons: vec![windows_icons::get_icon_by_process_id(value.process_id())],
focused_window_idx: 0,
}
}
}
impl KomorebiNotificationStateContainerInformation {
pub const EMPTY: Self = Self {
titles: vec![],
icons: vec![],
focused_window_idx: 0,
};
}

View File

@@ -0,0 +1,311 @@
use crate::config::DisplayFormat;
use crate::komorebi::KomorebiLayoutConfig;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use eframe::egui::vec2;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Label;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Monocle,
Floating,
Paused,
Custom,
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) =
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
{
return Ok(KomorebiLayout::Default(default_layout));
}
// Handle other cases
match s.as_str() {
"Monocle" => Ok(KomorebiLayout::Monocle),
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
}
}
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Monocle => write!(f, "Monocle"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiLayout {
fn is_default(&mut self) -> bool {
matches!(self, KomorebiLayout::Default(_))
}
fn on_click(
&mut self,
show_options: &bool,
monitor_idx: usize,
workspace_idx: Option<usize>,
) -> bool {
if self.is_default() {
!show_options
} else {
self.on_click_option(monitor_idx, workspace_idx);
false
}
}
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
match self {
KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx {
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
monitor_idx,
ws_idx,
*option,
))
.is_err()
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
}
}
KomorebiLayout::Monocle => {
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
tracing::error!("could not send message to komorebi: ToggleMonocle");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
fn show_icon(&mut self, font_id: FontId, ctx: &Context, ui: &mut Ui) {
// paint custom icons for the layout
let size = Vec2::splat(font_id.size);
let (response, painter) = ui.allocate_painter(size, Sense::hover());
let color = ctx.style().visuals.selection.stroke.color;
let stroke = Stroke::new(1.0, color);
let mut rect = response.rect;
let rounding = Rounding::same(rect.width() * 0.1);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke);
match self {
KomorebiLayout::Default(layout) => match layout {
komorebi_client::DefaultLayout::BSP => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Columns => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Rows => {
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
}
komorebi_client::DefaultLayout::VerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
}
komorebi_client::DefaultLayout::RightMainVerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(r, 0.0), c], stroke);
}
komorebi_client::DefaultLayout::HorizontalStack => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c, c + vec2(0.0, r)], stroke);
}
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Grid => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
}
},
KomorebiLayout::Monocle => {}
KomorebiLayout::Floating => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.5);
rect_left.set_height(rect.height() * 0.5);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.1 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.35 + stroke.width,
rect.width() * 0.35 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_stroke(rect_right, rounding, stroke);
}
KomorebiLayout::Paused => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.25);
rect_left.set_height(rect.height() * 0.8);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.2 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.55 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_filled(rect_right, rounding, color);
}
KomorebiLayout::Custom => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke);
painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke);
}
}
}
pub fn show(
&mut self,
ctx: &Context,
ui: &mut Ui,
render_config: &mut RenderConfig,
layout_config: &KomorebiLayoutConfig,
workspace_idx: Option<usize>,
) {
let monitor_idx = render_config.monitor_idx;
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
if !self.is_default() {
show_options = false;
}
render_config.apply_on_widget(false, ui, |ui| {
let layout_frame = SelectableFrame::new(false)
.show(ui, |ui| {
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
self.show_icon(font_id.clone(), ctx, ui);
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
ui.add(Label::new(self.to_string()).selectable(false));
}
})
.on_hover_text(self.to_string());
if layout_frame.clicked() {
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
}
if show_options {
if let Some(workspace_idx) = workspace_idx {
Frame::none().show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::HorizontalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
for layout_option in &mut layout_options {
if SelectableFrame::new(self == layout_option)
.show(ui, |ui| layout_option.show_icon(font_id.clone(), ctx, ui))
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
}
}
});
RenderConfig::store_show_komorebi_layout_options(show_options);
}
}

View File

@@ -1,11 +1,15 @@
mod bar;
mod battery;
mod config;
mod cpu;
mod date;
mod komorebi;
mod komorebi_layout;
mod media;
mod memory;
mod network;
mod render;
mod selected_frame;
mod storage;
mod time;
mod ui;
@@ -14,29 +18,40 @@ mod widget;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
use crate::config::Position;
use atomic_float::AtomicF32;
use crate::config::PositionConfig;
use clap::Parser;
use color_eyre::eyre::bail;
use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
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::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;
use windows::Win32::System::Threading::GetCurrentThreadId;
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;
pub static WIDGET_SPACING: f32 = 10.0;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static DPI: AtomicF32 = AtomicF32::new(1.0);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
#[derive(Parser)]
#[clap(author, about, version)]
@@ -53,36 +68,41 @@ struct Opts {
/// Write an example komorebi.bar.json to disk
#[clap(long)]
quickstart: bool,
/// Print a list of aliases that can be renamed to canonical variants
#[clap(long)]
#[clap(hide = true)]
aliases: bool,
}
macro_rules! as_ptr {
($value:expr) => {
$value as *mut core::ffi::c_void
};
}
pub fn dpi_for_monitor(hmonitor: isize) -> color_eyre::Result<f32> {
use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::UI::HiDpi::GetDpiForMonitor;
use windows::Win32::UI::HiDpi::MDT_EFFECTIVE_DPI;
let mut dpi_x = u32::default();
let mut dpi_y = u32::default();
extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
unsafe {
match GetDpiForMonitor(
HMONITOR(as_ptr!(hmonitor)),
MDT_EFFECTIVE_DPI,
std::ptr::addr_of_mut!(dpi_x),
std::ptr::addr_of_mut!(dpi_y),
) {
Ok(_) => {}
Err(error) => bail!(error),
let mut process_id = 0;
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
if process_id == GetCurrentProcessId() {
*(lparam.0 as *mut HWND) = hwnd;
BOOL::from(false) // Stop enumeration
} else {
BOOL::from(true) // Continue enumeration
}
}
}
#[allow(clippy::cast_precision_loss)]
Ok(dpi_y as f32 / 96.0)
fn process_hwnd() -> Option<isize> {
unsafe {
let mut hwnd = HWND::default();
let _ = EnumThreadWindows(
GetCurrentThreadId(),
Some(enum_window),
LPARAM(&mut hwnd as *mut HWND as isize),
);
if hwnd.0 as isize == 0 {
None
} else {
Some(hwnd.0 as isize)
}
}
}
fn main() -> color_eyre::Result<()> {
@@ -166,7 +186,7 @@ fn main() -> color_eyre::Result<()> {
Option::from,
);
let config = match config_path {
let mut config = match config_path {
None => {
let komorebi_bar_json =
include_str!("../../docs/komorebi.bar.example.json").to_string();
@@ -180,10 +200,12 @@ fn main() -> color_eyre::Result<()> {
KomobarConfig::read(&default_config_path)?
}
Some(ref config) => {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
if !opts.aliases {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
}
KomobarConfig::read(config)?
}
@@ -191,46 +213,67 @@ fn main() -> color_eyre::Result<()> {
let config_path = config_path.unwrap_or(default_config_path);
if opts.aliases {
KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?);
std::process::exit(0);
}
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
&SocketMessage::State,
)?)?;
let dpi = dpi_for_monitor(state.monitors.elements()[config.monitor.index].id())?;
DPI.store(dpi, Ordering::SeqCst);
MONITOR_RIGHT.store(
state.monitors.elements()[config.monitor.index].size().right,
Ordering::SeqCst,
);
let mut viewport_builder = ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_taskbar(false)
.with_position(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32 / dpi,
y: state.monitors.elements()[config.monitor.index].size().top as f32 / dpi,
})
.with_inner_size({
Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32 / dpi,
y: 50.0 / dpi,
}
});
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().top,
Ordering::SeqCst,
);
if let Some(viewport) = &config.viewport {
if let Some(mut position) = &viewport.position {
position.x /= dpi;
position.y /= dpi;
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().left,
Ordering::SeqCst,
);
let b = viewport_builder.clone();
viewport_builder = b.with_position(position);
MONITOR_INDEX.store(config.monitor.index, Ordering::SeqCst);
match config.position {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
}),
})
}
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
});
}
if let Some(mut inner_size) = &viewport.inner_size {
inner_size.x /= dpi;
inner_size.y /= dpi;
let b = viewport_builder.clone();
viewport_builder = b.with_inner_size(inner_size);
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
})
}
}
}
let viewport_builder = ViewportBuilder::default()
.with_decorations(false)
.with_transparent(true)
.with_taskbar(false);
let native_options = eframe::NativeOptions {
viewport: viewport_builder,
..Default::default()
@@ -291,7 +334,9 @@ fn main() -> color_eyre::Result<()> {
std::thread::spawn(move || {
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
let listener = komorebi_client::subscribe(&subscriber_name)
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
filter_state_changes: true,
})
.expect("could not subscribe to komorebi notifications");
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);
@@ -333,18 +378,21 @@ fn main() -> color_eyre::Result<()> {
match String::from_utf8(buffer) {
Ok(notification_string) => {
if let Ok(notification) =
serde_json::from_str::<komorebi_client::Notification>(
&notification_string,
)
{
tracing::debug!("received notification from komorebi");
match serde_json::from_str::<komorebi_client::Notification>(
&notification_string,
) {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(notification) {
tracing::error!("could not send komorebi notification update to gui: {error}")
if let Err(error) = tx_gui.send(notification) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}
ctx_komorebi.request_repaint();
}
Err(error) => {
tracing::error!("could not deserialize komorebi notification: {error}");
}
ctx_komorebi.request_repaint();
}
}
Err(error) => {

View File

@@ -1,7 +1,7 @@
use crate::render::RenderConfig;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -78,7 +78,7 @@ impl Media {
}
impl BarWidget for Media {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
@@ -102,26 +102,26 @@ impl BarWidget for Media {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
config.apply_on_widget(true, ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
self.toggle();
}
ui.add_space(WIDGET_SPACING);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
self.toggle();
}
});
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -23,6 +24,8 @@ pub struct MemoryConfig {
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<MemoryConfig> for Memory {
@@ -36,6 +39,7 @@ impl From<MemoryConfig> for Memory {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
@@ -45,6 +49,7 @@ pub struct Memory {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
@@ -58,12 +63,17 @@ impl Memory {
let used = self.system.used_memory();
let total = self.system.total_memory();
format!("RAM: {}%", (used * 100) / total)
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
}
}
}
impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
@@ -75,7 +85,12 @@ impl BarWidget for Memory {
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::MEMORY.to_string(),
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::MEMORY.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
@@ -87,22 +102,23 @@ impl BarWidget for Memory {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
eprintln!("{}", error)
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
}
});
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -30,85 +31,44 @@ pub struct NetworkConfig {
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let mut last_state_data = vec![];
let mut last_state_transmitted = vec![];
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
let mut networks_network_activity = Networks::new_with_refreshed_list();
let mut default_interface = String::new();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = interface.friendly_name {
default_interface.clone_from(&friendly_name);
if value.show_total_data_transmitted {
networks_total_data_transmitted.refresh();
for (interface_name, data) in &networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
last_state_data.push(format!(
"{} {} / {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
))
}
}
}
if value.show_network_activity {
networks_network_activity.refresh();
for (interface_name, data) in &networks_network_activity {
if friendly_name.eq(interface_name) {
last_state_transmitted.push(format!(
"{} {: >width$}/s {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width = value.network_activity_fill_characters.unwrap_or_default(),
))
}
}
}
}
}
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
networks_total_data_transmitted,
networks_network_activity,
default_interface,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
show_total_data_transmitted: value.show_total_data_transmitted,
show_network_activity: value.show_network_activity,
networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
show_total_activity: value.show_total_data_transmitted,
show_activity: value.show_network_activity,
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_data_transmitted: last_state_data,
last_state_network_activity: last_state_transmitted,
last_updated_total_data_transmitted: Instant::now(),
last_updated_network_activity: Instant::now(),
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_data_transmitted: bool,
pub show_network_activity: bool,
networks_total_data_transmitted: Networks,
pub show_total_activity: bool,
pub show_activity: bool,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_data_transmitted: Vec<String>,
last_state_network_activity: Vec<String>,
last_updated_total_data_transmitted: Instant,
last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
@@ -122,29 +82,44 @@ impl Network {
}
}
fn network_activity(&mut self) -> Vec<String> {
let mut outputs = self.last_state_network_activity.clone();
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now();
if self.show_network_activity
&& now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
activity.clear();
total_activity.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_network_activity {
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
outputs.push(format!(
"{} {: >width$}/s {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), self.data_refresh_interval),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), self.data_refresh_interval),
width = self.network_activity_fill_characters,
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if self.show_activity {
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
),
Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
),
));
}
if self.show_total_activity {
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
Self::to_pretty_bytes(data.total_received(), 1),
Self::to_pretty_bytes(data.total_transmitted(), 1),
))
}
}
@@ -152,67 +127,147 @@ impl Network {
}
}
self.last_state_network_activity.clone_from(&outputs);
self.last_state_activity.clone_from(&activity);
self.last_state_total_activity.clone_from(&total_activity);
self.last_updated_network_activity = now;
}
outputs
(activity, total_activity)
}
fn total_data_transmitted(&mut self) -> Vec<String> {
let mut outputs = self.last_state_total_data_transmitted.clone();
let now = Instant::now();
fn reading_to_label(&self, ctx: &Context, reading: NetworkReading) -> Label {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"{: >width$}/s | ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"{: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("{} | ", reading.received_text),
reading.transmitted_text,
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s | ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"UP: {: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s | ", reading.received_text),
format!("\u{2211}UP: {}/s", reading.transmitted_text),
),
},
};
if self.show_total_data_transmitted
&& now.duration_since(self.last_updated_total_data_transmitted)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_total_data_transmitted {
self.networks_total_data_transmitted.refresh();
let icon_format =
TextFormat::simple(font_id.clone(), ctx.style().visuals.selection.stroke.color);
let text_format = TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color());
for (interface_name, data) in &self.networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
outputs.push(format!(
"{} {} / {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
))
}
}
}
// icon
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
icon_format.font_id.clone(),
icon_format.color,
100.0,
);
self.last_state_total_data_transmitted.clone_from(&outputs);
self.last_updated_total_data_transmitted = now;
// text
layout_job.append(
&text_down,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
// icon
layout_job.append(
&match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
0.0,
icon_format.clone(),
);
// text
layout_job.append(
&text_up,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
Label::new(layout_job).selectable(false)
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
outputs
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.show_total_data_transmitted {
for output in self.total_data_transmitted() {
ui.add(Label::new(output).selectable(false));
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.show_total_activity || self.show_activity {
let (activity, total_activity) = self.network_activity();
if self.show_total_activity {
for reading in total_activity {
config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading));
});
}
}
ui.add_space(WIDGET_SPACING);
}
if self.show_network_activity {
for output in self.network_activity() {
ui.add(Label::new(output).selectable(false));
if self.show_activity {
for reading in activity {
config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading));
});
}
}
ui.add_space(WIDGET_SPACING);
}
if self.enable {
@@ -227,33 +282,65 @@ impl BarWidget for Network {
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::WIFI_HIGH.to_string(),
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error)
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error)
}
}
}
});
}
}
}
}
ui.add_space(WIDGET_SPACING);
#[derive(Clone)]
enum NetworkReadingFormat {
Speed = 0,
Total = 1,
}
#[derive(Clone)]
struct NetworkReading {
pub format: NetworkReadingFormat,
pub received_text: String,
pub transmitted_text: String,
}
impl NetworkReading {
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
NetworkReading {
format,
received_text: received,
transmitted_text: transmitted,
}
}
}
@@ -276,22 +363,3 @@ impl fmt::Display for DataUnit {
write!(f, "{:?}", self)
}
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}

283
komorebi-bar/src/render.rs Normal file
View File

@@ -0,0 +1,283 @@
use crate::bar::Alignment;
use crate::config::KomobarConfig;
use eframe::egui::Color32;
use eframe::egui::Frame;
use eframe::egui::InnerResponse;
use eframe::egui::Margin;
use eframe::egui::Rounding;
use eframe::egui::Shadow;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
pub enum Grouping {
/// No grouping is applied
None,
/// Widgets are grouped as a whole
Bar(GroupingConfig),
/// Widgets are grouped by alignment
Alignment(GroupingConfig),
/// Widgets are grouped individually
Widget(GroupingConfig),
}
#[derive(Copy, Clone)]
pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize,
/// Spacing between widgets
pub spacing: f32,
/// Sets how widgets are grouped
pub grouping: Grouping,
/// Background color
pub background_color: Color32,
/// Alignment of the widgets
pub alignment: Option<Alignment>,
/// Add more inner margin when adding a widget group
pub more_inner_margin: bool,
/// Set to true after the first time the apply_on_widget was called on an alignment
pub applied_on_widget: bool,
}
pub trait RenderExt {
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig;
}
impl RenderExt for &KomobarConfig {
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
RenderConfig {
monitor_idx: self.monitor.index,
spacing: self.widget_spacing.unwrap_or(10.0),
grouping: self.grouping.unwrap_or(Grouping::None),
background_color,
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
}
}
}
impl RenderConfig {
pub fn load_show_komorebi_layout_options() -> bool {
SHOW_KOMOREBI_LAYOUT_OPTIONS.load(Ordering::SeqCst) != 0
}
pub fn store_show_komorebi_layout_options(show: bool) {
SHOW_KOMOREBI_LAYOUT_OPTIONS.store(show as usize, Ordering::SeqCst);
}
pub fn new() -> Self {
Self {
monitor_idx: 0,
spacing: 0.0,
grouping: Grouping::None,
background_color: Color32::BLACK,
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
}
}
pub fn apply_on_bar<R>(
&mut self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.alignment = None;
if let Grouping::Bar(config) = self.grouping {
return self.define_group(None, config, ui, add_contents);
}
Self::fallback_group(ui, add_contents)
}
pub fn apply_on_alignment<R>(
&mut self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.alignment = None;
if let Grouping::Alignment(config) = self.grouping {
return self.define_group(None, config, ui, add_contents);
}
Self::fallback_group(ui, add_contents)
}
pub fn apply_on_widget<R>(
&mut self,
more_inner_margin: bool,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.more_inner_margin = more_inner_margin;
let outer_margin = self.widget_outer_margin(ui);
if let Grouping::Widget(config) = self.grouping {
return self.define_group(Some(outer_margin), config, ui, add_contents);
}
self.fallback_widget_group(Some(outer_margin), ui, add_contents)
}
fn fallback_group<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
InnerResponse {
inner: add_contents(ui),
response: ui.response().clone(),
}
}
fn fallback_widget_group<R>(
&mut self,
outer_margin: Option<Margin>,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
Frame::none()
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
.inner_margin(match self.more_inner_margin {
true => Margin::symmetric(5.0, 0.0),
false => Margin::same(0.0),
})
.show(ui, add_contents)
}
fn define_group<R>(
&mut self,
outer_margin: Option<Margin>,
config: GroupingConfig,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
Frame::group(ui.style_mut())
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
.inner_margin(match self.more_inner_margin {
true => Margin::symmetric(8.0, 3.0),
false => Margin::symmetric(3.0, 3.0),
})
.stroke(ui.style().visuals.widgets.noninteractive.bg_stroke)
.rounding(match config.rounding {
Some(rounding) => rounding.into(),
None => ui.style().visuals.widgets.noninteractive.rounding,
})
.fill(
self.background_color
.try_apply_alpha(config.transparency_alpha),
)
.shadow(match config.style {
Some(style) => match style {
// new styles can be added if needed here
GroupingStyle::Default => Shadow::NONE,
GroupingStyle::DefaultWithShadow => Shadow {
blur: 4.0,
offset: Vec2::new(1.0, 1.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
},
None => Shadow::NONE,
})
.show(ui, add_contents)
}
fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin {
let spacing = if self.applied_on_widget {
// Remove the default item spacing from the margin
self.spacing - ui.spacing().item_spacing.x
} else {
0.0
};
if !self.applied_on_widget {
self.applied_on_widget = true;
}
Margin {
left: match self.alignment {
Some(align) => match align {
Alignment::Left => spacing,
Alignment::Center => spacing,
Alignment::Right => 0.0,
},
None => 0.0,
},
right: match self.alignment {
Some(align) => match align {
Alignment::Left => 0.0,
Alignment::Center => 0.0,
Alignment::Right => spacing,
},
None => 0.0,
},
top: 0.0,
bottom: 0.0,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct GroupingConfig {
/// Styles for the grouping
pub style: Option<GroupingStyle>,
/// Alpha value for the color transparency [[0-255]] (default: 200)
pub transparency_alpha: Option<u8>,
/// Rounding values for the 4 corners. Can be a single or 4 values.
pub rounding: Option<RoundingConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum GroupingStyle {
#[serde(alias = "CtByte")]
Default,
/// A black shadow is added under the default group
#[serde(alias = "CtByteWithShadow")]
DefaultWithShadow,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum RoundingConfig {
/// All 4 corners are the same
Same(f32),
/// All 4 corners are custom. Order: NW, NE, SW, SE
Individual([f32; 4]),
}
impl From<RoundingConfig> for Rounding {
fn from(value: RoundingConfig) -> Self {
match value {
RoundingConfig::Same(value) => Rounding::same(value),
RoundingConfig::Individual(values) => Self {
nw: values[0],
ne: values[1],
sw: values[2],
se: values[3],
},
}
}
}
pub trait Color32Ext {
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self;
}
impl Color32Ext for Color32 {
/// Tries to apply the alpha value to the Color32
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self {
if let Some(alpha) = transparency_alpha {
return Color32::from_rgba_unmultiplied(self.r(), self.g(), self.b(), alpha);
}
self
}
}

View File

@@ -0,0 +1,55 @@
use eframe::egui::Frame;
use eframe::egui::Margin;
use eframe::egui::Response;
use eframe::egui::Sense;
use eframe::egui::Ui;
/// Same as SelectableLabel, but supports all content
pub struct SelectableFrame {
selected: bool,
}
impl SelectableFrame {
pub fn new(selected: bool) -> Self {
Self { selected }
}
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
let Self { selected } = self;
Frame::none()
.show(ui, |ui| {
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
if ui.is_rect_visible(response.rect) {
let inner_margin = Margin::symmetric(
ui.style().spacing.button_padding.x,
ui.style().spacing.button_padding.y,
);
if selected
|| response.hovered()
|| response.highlighted()
|| response.has_focus()
{
let visuals = ui.style().interact_selectable(&response, selected);
Frame::none()
.stroke(visuals.bg_stroke)
.rounding(visuals.rounding)
.fill(visuals.bg_fill)
.inner_margin(inner_margin)
.show(ui, add_contents);
} else {
Frame::none()
.inner_margin(inner_margin)
.show(ui, add_contents);
}
}
response
})
.inner
.on_hover_cursor(eframe::egui::CursorIcon::PointingHand)
}
}

View File

@@ -1,5 +1,6 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -22,6 +23,8 @@ pub struct StorageConfig {
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<StorageConfig> for Storage {
@@ -30,6 +33,7 @@ impl From<StorageConfig> for Storage {
enable: value.enable,
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
@@ -39,6 +43,7 @@ pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
@@ -58,11 +63,12 @@ impl Storage {
let available = disk.available_space();
let used = total - available;
disks.push(format!(
"{} {}%",
mount.to_string_lossy(),
(used * 100) / total
))
disks.push(match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
})
}
disks.sort();
@@ -73,7 +79,7 @@ impl Storage {
}
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let font_id = ctx
.style()
@@ -84,7 +90,12 @@ impl BarWidget for Storage {
for output in self.output() {
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::HARD_DRIVES.to_string(),
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::HARD_DRIVES.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
@@ -96,27 +107,27 @@ impl BarWidget for Storage {
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
eprintln!("{}", error)
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
});
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
@@ -18,6 +19,8 @@ pub struct TimeConfig {
pub enable: bool,
/// Set the Time format
pub format: TimeFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<TimeConfig> for Time {
@@ -25,6 +28,7 @@ impl From<TimeConfig> for Time {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
@@ -61,6 +65,7 @@ impl TimeFormat {
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
label_prefix: LabelPrefix,
}
impl Time {
@@ -72,9 +77,9 @@ impl Time {
}
impl BarWidget for Time {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
@@ -84,31 +89,40 @@ impl BarWidget for Time {
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::CLOCK.to_string(),
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CLOCK.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "TIME: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.toggle()
}
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.toggle()
}
});
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,5 +1,7 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::komorebi::Komorebi;
@@ -10,6 +12,7 @@ use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::render::RenderConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
@@ -21,12 +24,13 @@ use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui);
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
Date(DateConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
@@ -40,6 +44,7 @@ impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
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::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),

View File

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

View File

@@ -1,6 +1,8 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::config_generation::ApplicationConfiguration;
@@ -44,6 +46,7 @@ pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
use komorebi::DATA_DIR;
@@ -52,6 +55,7 @@ use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::Shutdown;
use std::time::Duration;
pub use uds_windows::UnixListener;
use uds_windows::UnixStream;
@@ -60,6 +64,7 @@ const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
@@ -67,6 +72,8 @@ pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_read_timeout(Some(Duration::from_secs(1)))?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
stream.shutdown(Shutdown::Write)?;
@@ -96,3 +103,29 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
Ok(listener)
}
pub fn subscribe_with_options(
name: &str,
options: SubscribeOptions,
) -> std::io::Result<UnixListener> {
let socket = DATA_DIR.join(name);
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {}
_ => {
return Err(error);
}
},
};
let listener = UnixListener::bind(&socket)?;
send_message(&SocketMessage::AddSubscriberSocketWithOptions(
name.to_string(),
options,
))?;
Ok(listener)
}

View File

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

View File

@@ -27,7 +27,6 @@ fn main() {
viewport: ViewportBuilder::default()
.with_always_on_top()
.with_inner_size([320.0, 500.0]),
follow_system_theme: true,
..Default::default()
};
@@ -234,7 +233,8 @@ extern "system" fn enum_window(
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
let language = "json";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
let theme =
egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), &ui.ctx().style());
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
}

View File

@@ -1,11 +1,14 @@
[package]
name = "komorebi-themes"
version = "0.1.30"
version = "0.1.31"
edition = "2021"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "a2c48f45782c5604bf5482d3873021a9fe45ea1a" }
catppuccin-egui = { version = "5.1", default-features = false, features = ["egui28"] }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "c11fbe2a3a4681485c5065b899a4c4d85fad3b04" }
#catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f579847bf2f552b144361d5a78ed8cf360b55cbb" }
catppuccin-egui = { version = "5", default-features = false, features = ["egui29"] }
eframe = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_variant = "0.1"
strum = "0.26"

View File

@@ -4,10 +4,12 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::IntoEnumIterator;
pub use base16_egui_themes::Base16;
pub use catppuccin_egui;
pub use eframe::egui::Color32;
use serde_variant::to_variant_name;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
@@ -24,6 +26,28 @@ pub enum Theme {
},
}
impl Theme {
pub fn variant_names(&self) -> Vec<String> {
match self {
Theme::Catppuccin { .. } => {
vec![
"Frappe".to_string(),
"Latte".to_string(),
"Macchiato".to_string(),
"Mocha".to_string(),
]
}
Theme::Base16 { .. } => Base16::iter()
.map(|variant| {
to_variant_name(&variant)
.expect("could not convert to variant name")
.to_string()
})
.collect(),
}
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub enum Base16Value {
Base00,
@@ -124,35 +148,39 @@ pub enum CatppuccinValue {
Crust,
}
pub fn color32_compat(rgba: [u8; 4]) -> Color32 {
Color32::from_rgba_unmultiplied(rgba[0], rgba[1], rgba[2], rgba[3])
}
impl CatppuccinValue {
pub fn color32(&self, theme: catppuccin_egui::Theme) -> Color32 {
match self {
CatppuccinValue::Rosewater => theme.rosewater,
CatppuccinValue::Flamingo => theme.flamingo,
CatppuccinValue::Pink => theme.pink,
CatppuccinValue::Mauve => theme.mauve,
CatppuccinValue::Red => theme.red,
CatppuccinValue::Maroon => theme.maroon,
CatppuccinValue::Peach => theme.peach,
CatppuccinValue::Yellow => theme.yellow,
CatppuccinValue::Green => theme.green,
CatppuccinValue::Teal => theme.teal,
CatppuccinValue::Sky => theme.sky,
CatppuccinValue::Sapphire => theme.sapphire,
CatppuccinValue::Blue => theme.blue,
CatppuccinValue::Lavender => theme.lavender,
CatppuccinValue::Text => theme.text,
CatppuccinValue::Subtext1 => theme.subtext1,
CatppuccinValue::Subtext0 => theme.subtext0,
CatppuccinValue::Overlay2 => theme.overlay2,
CatppuccinValue::Overlay1 => theme.overlay1,
CatppuccinValue::Overlay0 => theme.overlay0,
CatppuccinValue::Surface2 => theme.surface2,
CatppuccinValue::Surface1 => theme.surface1,
CatppuccinValue::Surface0 => theme.surface0,
CatppuccinValue::Base => theme.base,
CatppuccinValue::Mantle => theme.mantle,
CatppuccinValue::Crust => theme.crust,
CatppuccinValue::Rosewater => color32_compat(theme.rosewater.to_srgba_unmultiplied()),
CatppuccinValue::Flamingo => color32_compat(theme.flamingo.to_srgba_unmultiplied()),
CatppuccinValue::Pink => color32_compat(theme.pink.to_srgba_unmultiplied()),
CatppuccinValue::Mauve => color32_compat(theme.mauve.to_srgba_unmultiplied()),
CatppuccinValue::Red => color32_compat(theme.red.to_srgba_unmultiplied()),
CatppuccinValue::Maroon => color32_compat(theme.maroon.to_srgba_unmultiplied()),
CatppuccinValue::Peach => color32_compat(theme.peach.to_srgba_unmultiplied()),
CatppuccinValue::Yellow => color32_compat(theme.yellow.to_srgba_unmultiplied()),
CatppuccinValue::Green => color32_compat(theme.green.to_srgba_unmultiplied()),
CatppuccinValue::Teal => color32_compat(theme.teal.to_srgba_unmultiplied()),
CatppuccinValue::Sky => color32_compat(theme.sky.to_srgba_unmultiplied()),
CatppuccinValue::Sapphire => color32_compat(theme.sapphire.to_srgba_unmultiplied()),
CatppuccinValue::Blue => color32_compat(theme.blue.to_srgba_unmultiplied()),
CatppuccinValue::Lavender => color32_compat(theme.lavender.to_srgba_unmultiplied()),
CatppuccinValue::Text => color32_compat(theme.text.to_srgba_unmultiplied()),
CatppuccinValue::Subtext1 => color32_compat(theme.subtext1.to_srgba_unmultiplied()),
CatppuccinValue::Subtext0 => color32_compat(theme.subtext0.to_srgba_unmultiplied()),
CatppuccinValue::Overlay2 => color32_compat(theme.overlay2.to_srgba_unmultiplied()),
CatppuccinValue::Overlay1 => color32_compat(theme.overlay1.to_srgba_unmultiplied()),
CatppuccinValue::Overlay0 => color32_compat(theme.overlay0.to_srgba_unmultiplied()),
CatppuccinValue::Surface2 => color32_compat(theme.surface2.to_srgba_unmultiplied()),
CatppuccinValue::Surface1 => color32_compat(theme.surface1.to_srgba_unmultiplied()),
CatppuccinValue::Surface0 => color32_compat(theme.surface0.to_srgba_unmultiplied()),
CatppuccinValue::Base => color32_compat(theme.base.to_srgba_unmultiplied()),
CatppuccinValue::Mantle => color32_compat(theme.mantle.to_srgba_unmultiplied()),
CatppuccinValue::Crust => color32_compat(theme.crust.to_srgba_unmultiplied()),
}
}
}

View File

@@ -1,11 +1,9 @@
[package]
name = "komorebi"
version = "0.1.30"
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
version = "0.1.31"
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
license = "MIT"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -44,7 +42,6 @@ tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
uds_windows = { workspace = true }
which = { workspace = true }
widestring = "1"
win32-display-data = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }

View File

@@ -0,0 +1,115 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use super::prefix::AnimationPrefix;
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}
#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<String, AnimationState>,
}
impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}
impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}
pub fn is_cancelled(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.pending_cancel_count > 0
} else {
false
}
}
pub fn in_progress(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.in_progress
} else {
false
}
}
pub fn init_cancel(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;
// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn latest_cancel_idx(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn end_cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count -= 1;
}
}
pub fn cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;
}
}
pub fn start(&mut self, animation_key: &str) {
if let Entry::Vacant(e) = self.animations.entry(animation_key.to_string()) {
e.insert(AnimationState {
in_progress: true,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});
return;
}
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = true;
}
}
pub fn end(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;
if animation_state.pending_cancel_count == 0 {
self.animations.remove(animation_key);
}
}
}
pub fn count_in_progress(&self, animation_key_prefix: AnimationPrefix) -> usize {
self.animations
.keys()
.filter(|key| key.starts_with(animation_key_prefix.to_string().as_str()))
.count()
}
pub fn count(&self) -> usize {
self.animations.len()
}
}

View File

@@ -0,0 +1,122 @@
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use super::RenderDispatcher;
use super::ANIMATION_DURATION_GLOBAL;
use super::ANIMATION_FPS;
use super::ANIMATION_MANAGER;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct AnimationEngine;
impl AnimationEngine {
pub fn wait_for_all_animations() {
let max_duration = Duration::from_secs(20);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().count() > 0 {
if spent_duration.elapsed() >= max_duration {
break;
}
std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst),
));
}
}
/// Returns true if the animation needs to continue
pub fn cancel(animation_key: &str) -> bool {
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(animation_key);
let max_duration = Duration::from_secs(5);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().in_progress(animation_key) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(animation_key);
}
std::thread::sleep(Duration::from_millis(250 / 2));
}
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(animation_key);
ANIMATION_MANAGER.lock().end_cancel(animation_key);
latest_cancel_idx == cancel_idx
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
render_dispatcher: (impl RenderDispatcher + Send + 'static),
duration: Duration,
) -> Result<()> {
std::thread::spawn(move || {
let animation_key = render_dispatcher.get_animation_key();
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
let should_animate = Self::cancel(animation_key.as_str());
if !should_animate {
return Ok(());
}
}
render_dispatcher.pre_render()?;
ANIMATION_MANAGER.lock().start(animation_key.as_str());
let target_frame_time =
Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER
.lock()
.is_cancelled(animation_key.as_str())
{
// cancel animation
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
return Ok(());
}
let frame_start = Instant::now();
// calculate progress
progress =
animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_dispatcher.render(progress).ok();
// sleep until next frame
let frame_time_elapsed = frame_start.elapsed();
if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}
ANIMATION_MANAGER.lock().end(animation_key.as_str());
// limit progress to 1.0 if animation took longer
if progress != 1.0 {
progress = 1.0;
// process animation for 1.0 to set target position
render_dispatcher.render(progress).ok();
}
render_dispatcher.post_render()
});
Ok(())
}
}

View File

@@ -0,0 +1,42 @@
use crate::core::Rect;
use crate::AnimationStyle;
use super::style::apply_ease_func;
pub trait Lerp<T = Self> {
fn lerp(self, end: T, time: f64, style: AnimationStyle) -> T;
}
impl Lerp for i32 {
#[allow(clippy::cast_possible_truncation)]
fn lerp(self, end: i32, time: f64, style: AnimationStyle) -> i32 {
let time = apply_ease_func(time, style);
f64::from(end - self).mul_add(time, f64::from(self)).round() as i32
}
}
impl Lerp for f64 {
fn lerp(self, end: f64, time: f64, style: AnimationStyle) -> f64 {
let time = apply_ease_func(time, style);
(end - self).mul_add(time, self)
}
}
impl Lerp for u8 {
fn lerp(self, end: u8, time: f64, style: AnimationStyle) -> u8 {
(self as f64).lerp(end as f64, time, style) as u8
}
}
impl Lerp for Rect {
fn lerp(self, end: Rect, time: f64, style: AnimationStyle) -> Rect {
Rect {
left: self.left.lerp(end.left, time, style),
top: self.top.lerp(end.top, time, style),
right: self.right.lerp(end.right, time, style),
bottom: self.bottom.lerp(end.bottom, time, style),
}
}
}

View File

@@ -0,0 +1,54 @@
use crate::animation::animation_manager::AnimationManager;
use crate::core::animation::AnimationStyle;
use lazy_static::lazy_static;
use prefix::AnimationPrefix;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use parking_lot::Mutex;
pub use engine::AnimationEngine;
pub mod animation_manager;
pub mod engine;
pub mod lerp;
pub mod prefix;
pub mod render_dispatcher;
pub use render_dispatcher::RenderDispatcher;
pub mod style;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum PerAnimationPrefixConfig<T> {
Prefix(HashMap<AnimationPrefix, T>),
Global(T),
}
pub const DEFAULT_ANIMATION_ENABLED: bool = false;
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
lazy_static! {
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
pub static ref ANIMATION_STYLE_GLOBAL: Arc<Mutex<AnimationStyle>> =
Arc::new(Mutex::new(DEFAULT_ANIMATION_STYLE));
pub static ref ANIMATION_ENABLED_GLOBAL: Arc<AtomicBool> =
Arc::new(AtomicBool::new(DEFAULT_ANIMATION_ENABLED));
pub static ref ANIMATION_DURATION_GLOBAL: Arc<AtomicU64> =
Arc::new(AtomicU64::new(DEFAULT_ANIMATION_DURATION));
pub static ref ANIMATION_STYLE_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, AnimationStyle>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_ENABLED_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, bool>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_DURATION_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, u64>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);

View File

@@ -0,0 +1,31 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(
Copy,
Clone,
Debug,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum AnimationPrefix {
Movement,
Transparency,
}
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
format!("{}:{}", prefix, key)
}

View File

@@ -0,0 +1,8 @@
use color_eyre::Result;
pub trait RenderDispatcher {
fn get_animation_key(&self) -> String;
fn pre_render(&self) -> Result<()>;
fn render(&self, delta: f64) -> Result<()>;
fn post_render(&self) -> Result<()>;
}

View File

@@ -1,22 +1,6 @@
use crate::core::AnimationStyle;
use crate::core::Rect;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::f64::consts::PI;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_MANAGER;
use crate::ANIMATION_STYLE;
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
pub trait Ease {
fn evaluate(t: f64) -> f64;
@@ -370,9 +354,8 @@ impl Ease for EaseInOutBounce {
}
}
}
fn apply_ease_func(t: f64) -> f64 {
let style = *ANIMATION_STYLE.lock();
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
match style {
AnimationStyle::Linear => Linear::evaluate(t),
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
@@ -406,112 +389,3 @@ fn apply_ease_func(t: f64) -> f64 {
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
}
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct Animation {
pub hwnd: isize,
}
impl Animation {
pub fn new(hwnd: isize) -> Self {
Self { hwnd }
}
/// Returns true if the animation needs to continue
pub fn cancel(&mut self) -> bool {
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
return true;
}
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
let max_duration = Duration::from_secs(1);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(self.hwnd);
}
std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
));
}
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
latest_cancel_idx == cancel_idx
}
#[allow(clippy::cast_possible_truncation)]
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
let time = apply_ease_func(t);
f64::from(end - start)
.mul_add(time, f64::from(start))
.round() as i32
}
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
Rect {
left: Self::lerp(start_rect.left, end_rect.left, t),
top: Self::lerp(start_rect.top, end_rect.top, t),
right: Self::lerp(start_rect.right, end_rect.right, t),
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
}
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
&mut self,
duration: Duration,
mut render_callback: impl FnMut(f64) -> Result<()>,
) -> Result<()> {
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
let should_animate = self.cancel();
if !should_animate {
return Ok(());
}
}
ANIMATION_MANAGER.lock().start(self.hwnd);
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
// cancel animation
ANIMATION_MANAGER.lock().cancel(self.hwnd);
return Ok(());
}
let frame_start = Instant::now();
// calculate progress
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_callback(progress).ok();
// sleep until next frame
let frame_time_elapsed = frame_start.elapsed();
if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}
ANIMATION_MANAGER.lock().end(self.hwnd);
// limit progress to 1.0 if animation took longer
if progress > 1.0 {
progress = 1.0;
}
// process animation for 1.0 to set target position
render_callback(progress)
}
}

View File

@@ -1,108 +0,0 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}
#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<isize, AnimationState>,
}
impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}
impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}
pub fn is_cancelled(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.pending_cancel_count > 0
} else {
false
}
}
pub fn in_progress(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.in_progress
} else {
false
}
}
pub fn init_cancel(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;
// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn latest_cancel_idx(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn end_cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count -= 1;
}
}
pub fn cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
}
}
pub fn start(&mut self, hwnd: isize) {
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
e.insert(AnimationState {
in_progress: true,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
return;
}
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = true;
}
}
pub fn end(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
if animation_state.pending_cancel_count == 0 {
self.animations.remove(&hwnd);
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
}
}
}
}

View File

@@ -3,47 +3,86 @@ use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUS_STATE;
use crate::border_manager::RENDER_TARGETS;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::core::BorderStyle;
use crate::core::Rect;
use crate::windows_api;
use crate::WindowsApi;
use crate::WINDOWS_11;
use crate::core::BorderStyle;
use crate::core::Rect;
use color_eyre::eyre::anyhow;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use std::sync::LazyLock;
use std::sync::OnceLock;
use windows::Foundation::Numerics::Matrix3x2;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::FALSE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::TRUE;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreatePen;
use windows::Win32::Graphics::Gdi::DeleteObject;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
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;
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
use windows::Win32::Graphics::Direct2D::D2D1_HWND_RENDER_TARGET_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
use windows::Win32::Graphics::Gdi::CreateRectRgn;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::Rectangle;
use windows::Win32::Graphics::Gdi::RoundRect;
use windows::Win32::Graphics::Gdi::SelectObject;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
use windows::Win32::Graphics::Gdi::PS_SOLID;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
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::CREATESTRUCTW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
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::PCWSTR;
#[allow(clippy::expect_used)]
static RENDER_FACTORY: LazyLock<ID2D1Factory> = unsafe {
LazyLock::new(|| {
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
.expect("creating RENDER_FACTORY failed")
})
};
static BRUSH_PROPERTIES: LazyLock<D2D1_BRUSH_PROPERTIES> =
LazyLock::new(|| D2D1_BRUSH_PROPERTIES {
opacity: 1.0,
transform: Matrix3x2::identity(),
});
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
@@ -58,14 +97,36 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Border {
pub hwnd: isize,
pub render_target: OnceLock<ID2D1HwndRenderTarget>,
pub tracking_hwnd: isize,
pub window_rect: Rect,
pub window_kind: WindowKind,
pub style: BorderStyle,
pub width: i32,
pub offset: i32,
pub brush_properties: D2D1_BRUSH_PROPERTIES,
pub rounded_rect: D2D1_ROUNDED_RECT,
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
}
impl From<isize> for Border {
fn from(value: isize) -> Self {
Self { hwnd: value }
Self {
hwnd: value,
render_target: OnceLock::new(),
tracking_hwnd: 0,
window_rect: Rect::default(),
window_kind: WindowKind::Unfocused,
style: STYLE.load(),
width: BORDER_WIDTH.load(Ordering::Relaxed),
offset: BORDER_OFFSET.load(Ordering::Relaxed),
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
rounded_rect: D2D1_ROUNDED_RECT::default(),
brushes: HashMap::new(),
}
}
}
@@ -74,7 +135,7 @@ impl Border {
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
@@ -83,7 +144,6 @@ impl Border {
let window_class = WNDCLASSW {
hInstance: h_module.into(),
lpszClassName: class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(Self::callback),
hbrBackground: WindowsApi::create_solid_brush(0),
..Default::default()
@@ -91,12 +151,30 @@ impl Border {
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let (border_sender, border_receiver) = mpsc::channel();
let instance = h_module.0 as isize;
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
hwnd_sender.send(hwnd)?;
let mut border = Self {
hwnd: 0,
render_target: OnceLock::new(),
tracking_hwnd,
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
window_kind: WindowKind::Unfocused,
style: STYLE.load(),
width: BORDER_WIDTH.load(Ordering::Relaxed),
offset: BORDER_OFFSET.load(Ordering::Relaxed),
brush_properties: Default::default(),
rounded_rect: Default::default(),
brushes: HashMap::new(),
};
let border_pointer = std::ptr::addr_of!(border);
let hwnd =
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
border.hwnd = hwnd;
border_sender.send(border_pointer as isize)?;
let mut msg: MSG = MSG::default();
@@ -110,42 +188,117 @@ impl Border {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
std::thread::sleep(Duration::from_millis(10))
}
Ok(())
});
Ok(Self {
hwnd: hwnd_receiver.recv()?,
})
let border_ref = border_receiver.recv()?;
let border = unsafe { &mut *(border_ref as *mut Border) };
// I have literally no idea, apparently this is to get rid of the black pixels
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
unsafe {
let pos: i32 = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
let hrgn = CreateRectRgn(pos, 0, pos + 1, 1);
let mut bh: DWM_BLURBEHIND = Default::default();
if !hrgn.is_invalid() {
bh = DWM_BLURBEHIND {
dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION,
fEnable: TRUE,
hRgnBlur: hrgn,
fTransitionOnMaximized: FALSE,
};
}
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
}
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
pixelSize: Default::default(),
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
};
let render_target_properties = D2D1_RENDER_TARGET_PROPERTIES {
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_UNKNOWN,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 96.0,
dpiY: 96.0,
..Default::default()
};
match unsafe {
RENDER_FACTORY
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
} {
Ok(render_target) => unsafe {
border.brush_properties = *BRUSH_PROPERTIES.deref();
for window_kind in [
WindowKind::Single,
WindowKind::Stack,
WindowKind::Monocle,
WindowKind::Unfocused,
WindowKind::Floating,
] {
let color = window_kind_colour(window_kind);
let color = D2D1_COLOR_F {
r: ((color & 0xFF) as f32) / 255.0,
g: (((color >> 8) & 0xFF) as f32) / 255.0,
b: (((color >> 16) & 0xFF) as f32) / 255.0,
a: 1.0,
};
if let Ok(brush) =
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
{
border.brushes.insert(window_kind, brush);
}
}
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
if border.render_target.set(render_target.clone()).is_err() {
return Err(anyhow!("could not store border render target"));
}
border.rounded_rect = {
let radius = 8.0 + border.width as f32 / 2.0;
D2D1_ROUNDED_RECT {
rect: Default::default(),
radiusX: radius,
radiusY: radius,
}
};
let mut render_targets = RENDER_TARGETS.lock();
render_targets.insert(border.hwnd, render_target);
Ok(border.clone())
},
Err(error) => Err(error.into()),
}
}
pub fn destroy(&self) -> color_eyre::Result<()> {
let mut render_targets = RENDER_TARGETS.lock();
render_targets.remove(&self.hwnd);
WindowsApi::close_window(self.hwnd)
}
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
// Make adjustments to the border
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
let mut rect = *rect;
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
rect.add_margin(self.width);
rect.add_padding(-self.offset);
// Update the position of the border if required
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
should_invalidate = true;
}
// Invalidate the rect to trigger the callback to update colours etc.
if should_invalidate {
self.invalidate();
}
WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?;
Ok(())
}
// this triggers WM_PAINT in the callback below
pub fn invalidate(&self) {
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
}
@@ -158,75 +311,196 @@ impl Border {
) -> LRESULT {
unsafe {
match message {
WM_PAINT => {
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
WM_CREATE => {
let mut border_pointer: *mut Border =
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
// With the rect that we set in Self::update
match WindowsApi::window_rect(window.0 as isize) {
Ok(rect) => {
// Grab the focus kind for this border
let window_kind = {
FOCUS_STATE
.lock()
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused)
if border_pointer.is_null() {
let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _;
border_pointer = (*create_struct).lpCreateParams as *mut _;
SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _);
}
LRESULT(0)
}
EVENT_OBJECT_DESTROY => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
// we don't actually want to destroy the window here, just hide it for quicker
// visual feedback to the user; the actual destruction will be handled by the
// core border manager loop
WindowsApi::hide_window(window.0 as isize);
LRESULT(0)
}
EVENT_OBJECT_LOCATIONCHANGE => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
let reference_hwnd = lparam.0;
let old_rect = (*border_pointer).window_rect;
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
(*border_pointer).window_rect = rect;
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
tracing::error!("failed to update border position {error}");
}
if !rect.is_same_size_as(&old_rect) {
if let Some(render_target) = (*border_pointer).render_target.get() {
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
// Set up the brush to draw the border
let hpen = CreatePen(
PS_SOLID | PS_INSIDEFRAME,
BORDER_WIDTH.load(Ordering::SeqCst),
COLORREF(window_kind_colour(window_kind)),
);
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
let hbrush = WindowsApi::create_solid_brush(0);
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
// Draw the border
SelectObject(hdc, hpen);
SelectObject(hdc, hbrush);
// TODO(raggi): this is approximately the correct curvature for
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
// often the bottom right has a different shape. Furthermore if
// the window was made with DWMWCP_ROUNDSMALL then this is the
// wrong size. In the future we should read the DWM properties
// of windows and attempt to match appropriately.
match STYLE.load() {
BorderStyle::System => {
if *WINDOWS_11 {
// TODO: error handling
let _ =
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
} else {
// TODO: error handling
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
BorderStyle::Rounded => {
// TODO: error handling
let _ = RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
}
BorderStyle::Square => {
// TODO: error handling
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
}
let _ = render_target.EndDraw(None, None);
}
// TODO: error handling
let _ = DeleteObject(hpen);
// TODO: error handling
let _ = DeleteObject(hbrush);
}
Err(error) => {
tracing::error!("could not get border rect: {}", error.to_string())
}
}
// TODO: error handling
let _ = EndPaint(window, &ps);
LRESULT(0)
}
WM_PAINT => {
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
let border_pointer: *mut Border =
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
if let Some(render_target) = (*border_pointer).render_target.get() {
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
// Get window kind and color
(*border_pointer).window_kind = FOCUS_STATE
.lock()
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused);
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
(*border_pointer).style = STYLE.load();
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
let _ = render_target.EndDraw(None, None);
}
}
}
let _ = ValidateRect(window, None);
LRESULT(0)
}
WM_DESTROY => {
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
PostQuitMessage(0);
LRESULT(0)
}

View File

@@ -1,7 +1,6 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod border;
use crate::core::BorderImplementation;
use crate::core::BorderStyle;
use crate::core::WindowKind;
@@ -12,7 +11,7 @@ use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
use border::Border;
pub use border::Border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
@@ -30,15 +29,14 @@ use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
lazy_static! {
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
AtomicCell::new(BorderImplementation::Komorebi);
@@ -49,15 +47,20 @@ lazy_static! {
pub static ref MONOCLE: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
pub static ref FLOATING: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(245, 245, 165))));
}
lazy_static! {
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref 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());
}
pub struct Notification;
pub struct Notification(pub Option<isize>);
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
@@ -73,8 +76,12 @@ fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification() {
if event_tx().try_send(Notification).is_err() {
pub fn window_border(hwnd: isize) -> Option<Border> {
WINDOWS_BORDERS.lock().get(&hwnd).cloned()
}
pub fn send_notification(hwnd: Option<isize>) {
if event_tx().try_send(Notification(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
@@ -87,12 +94,13 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
);
for (_, border) in borders.iter() {
border.destroy()?;
let _ = border.destroy();
}
borders.clear();
BORDERS_MONITORS.lock().clear();
FOCUS_STATE.lock().clear();
RENDER_TARGETS.lock().clear();
let mut remaining_hwnds = vec![];
@@ -105,7 +113,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
for hwnd in remaining_hwnds {
Border::from(hwnd).destroy()?;
let _ = Border::from(hwnd).destroy();
}
}
@@ -114,10 +122,11 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
match focus_kind {
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed),
WindowKind::Single => FOCUSED.load(Ordering::Relaxed),
WindowKind::Stack => STACK.load(Ordering::Relaxed),
WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed),
WindowKind::Floating => FLOATING.load(Ordering::Relaxed),
}
}
@@ -137,21 +146,30 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
let receiver = event_rx();
event_tx().send(Notification)?;
event_tx().send(Notification(None))?;
let mut previous_snapshot = Ring::default();
let mut previous_pending_move_op = None;
let mut previous_is_paused = false;
let mut previous_notification: Option<Notification> = None;
'receiver: for _ in receiver {
'receiver: for notification in receiver {
// Check the wm state every time we receive a notification
let state = wm.lock();
let is_paused = state.is_paused;
let focused_monitor_idx = state.focused_monitor_idx();
let focused_workspace_idx =
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
let monitors = state.monitors.clone();
let pending_move_op = state.pending_move_op;
let pending_move_op = *state.pending_move_op;
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
[focused_workspace_idx]
.floating_windows()
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
drop(state);
match IMPLEMENTATION.load() {
@@ -220,6 +238,21 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
should_process_notification = true;
}
// when we switch focus to a floating window
if !should_process_notification
&& floating_window_hwnds.contains(&notification.0.unwrap_or_default())
{
should_process_notification = true;
}
if !should_process_notification {
if let Some(ref previous) = previous_notification {
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
should_process_notification = true;
}
}
}
if !should_process_notification {
tracing::trace!("monitor state matches latest snapshot, skipping notification");
continue 'receiver;
@@ -227,11 +260,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();
let mut windows_borders = WINDOWS_BORDERS.lock();
// If borders are disabled
if !BORDER_ENABLED.load_consume()
// Or if they are temporarily disabled
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
// Or if the wm is paused
|| is_paused
// Or if we are handling an alt-tab across workspaces
@@ -243,6 +275,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
borders.clear();
windows_borders.clear();
previous_is_paused = is_paused;
continue 'receiver;
@@ -275,7 +308,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let border = match borders.entry(monocle.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(monocle.id()) {
if let Ok(border) = Border::create(
monocle.id(),
monocle.focused_window().copied().unwrap_or_default().hwnd,
) {
entry.insert(border)
} else {
continue 'monitors;
@@ -284,6 +320,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
};
borders_monitors.insert(monocle.id().clone(), monitor_idx);
windows_borders.insert(
monocle.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
{
let mut focus_state = FOCUS_STATE.lock();
@@ -297,11 +337,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
);
}
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd,
)?;
let reference_hwnd =
monocle.focused_window().copied().unwrap_or_default().hwnd;
border.update(&rect, true)?;
let rect = WindowsApi::window_rect(reference_hwnd)?;
border.set_position(&rect, reference_hwnd)?;
border.invalidate();
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
@@ -322,9 +365,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
continue 'monitors;
}
let is_maximized = WindowsApi::is_zoomed(
WindowsApi::foreground_window().unwrap_or_default(),
);
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
let foreground_monitor_id =
WindowsApi::monitor_from_window(foreground_hwnd);
let is_maximized = foreground_monitor_id == m.id()
&& WindowsApi::is_zoomed(foreground_hwnd);
if is_maximized {
let mut to_remove = vec![];
@@ -345,16 +390,20 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
// Destroy any borders not associated with the focused workspace
let container_ids = ws
let mut container_and_floating_window_ids = ws
.containers()
.iter()
.map(|c| c.id().clone())
.collect::<Vec<_>>();
for w in ws.floating_windows() {
container_and_floating_window_ids.push(w.hwnd.to_string());
}
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
&& !container_ids.contains(id)
&& !container_and_floating_window_ids.contains(id)
{
border.destroy()?;
to_remove.push(id.clone());
@@ -366,47 +415,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
for (idx, c) in ws.containers().iter().enumerate() {
// Update border when moving or resizing with mouse
if pending_move_op.is_some() && idx == ws.focused_container_idx() {
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
let mut rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
while WindowsApi::lbutton_is_pressed() {
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'monitors;
}
}
};
let new_rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect, true)?;
}
}
Z_ORDER.store(restore_z_order);
continue 'monitors;
}
// Get the border entry for this container from the map or create one
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
if let Ok(border) = Border::create(
c.id(),
c.focused_window().copied().unwrap_or_default().hwnd,
) {
entry.insert(border)
} else {
continue 'monitors;
@@ -415,6 +431,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
};
borders_monitors.insert(c.id().clone(), monitor_idx);
windows_borders.insert(
c.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
#[allow(unused_assignments)]
let mut last_focus_state = None;
@@ -435,16 +455,79 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
}
let rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
let reference_hwnd =
c.focused_window().copied().unwrap_or_default().hwnd;
let rect = WindowsApi::window_rect(reference_hwnd)?;
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => last_focus_state != new_focus_state,
};
border.update(&rect, should_invalidate)?;
border.set_position(&rect, reference_hwnd)?;
if should_invalidate {
border.invalidate();
}
}
{
for window in ws.floating_windows() {
let border = match borders.entry(window.hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) =
Border::create(&window.hwnd.to_string(), window.hwnd)
{
entry.insert(border)
} else {
continue 'monitors;
}
}
};
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
windows_borders.insert(window.hwnd, border.clone());
let mut is_focused = false;
if WindowsApi::foreground_window().unwrap_or_default()
== window.hwnd
{
is_focused = true;
}
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = if is_focused {
WindowKind::Floating
} else {
WindowKind::Unfocused
};
// Update the focused state for all containers on this workspace
{
let mut focus_state = FOCUS_STATE.lock();
last_focus_state =
focus_state.insert(border.hwnd, new_focus_state);
}
let rect = WindowsApi::window_rect(window.hwnd)?;
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => last_focus_state != new_focus_state,
};
// this has to be sent to match the z-order when clicking on
// a floating window to focus it
border.set_position(&rect, window.hwnd)?;
if should_invalidate {
border.invalidate();
}
}
}
}
}
@@ -454,6 +537,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
previous_snapshot = monitors;
previous_pending_move_op = pending_move_op;
previous_is_paused = is_paused;
previous_notification = Some(notification);
}
Ok(())

View File

@@ -39,6 +39,18 @@ impl From<Color32> for Colour {
}
}
impl From<Colour> for Color32 {
fn from(value: Colour) -> Self {
match value {
Colour::Rgb(rgb) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
Colour::Hex(hex) => {
let rgb = Rgb::from(hex);
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
}
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Hex(HexColor);

View File

@@ -44,7 +44,7 @@ impl<'a, T: Clone> ComIn<'a, T> {
}
}
impl<'a, T> Deref for ComIn<'a, T> {
impl<T> Deref for ComIn<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data

View File

@@ -9,7 +9,7 @@ use serde::Serialize;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
pub struct Container {
#[getset(get = "pub")]
id: String,
@@ -27,12 +27,6 @@ impl Default for Container {
}
}
impl PartialEq for Container {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Container {
pub fn hide(&self, omit: Option<isize>) {
for window in self.windows().iter().rev() {

135
komorebi/src/core/asc.rs Normal file
View File

@@ -0,0 +1,135 @@
use crate::config_generation::ApplicationConfiguration;
use crate::config_generation::ApplicationOptions;
use crate::config_generation::MatchingRule;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ApplicationSpecificConfiguration(pub BTreeMap<String, AscApplicationRulesOrSchema>);
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum AscApplicationRulesOrSchema {
AscApplicationRules(AscApplicationRules),
Schema(String),
}
impl Deref for ApplicationSpecificConfiguration {
type Target = BTreeMap<String, AscApplicationRulesOrSchema>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ApplicationSpecificConfiguration {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl ApplicationSpecificConfiguration {
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(pathbuf)?;
Ok(serde_json::from_str(&content)?)
}
pub fn format(pathbuf: &PathBuf) -> Result<String> {
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
}
}
/// Rules that determine how an application is handled
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct AscApplicationRules {
/// Rules to ignore specific windows
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<Vec<MatchingRule>>,
/// Rules to forcibly manage specific windows
#[serde(skip_serializing_if = "Option::is_none")]
pub manage: Option<Vec<MatchingRule>>,
/// Rules to manage specific windows as floating windows
#[serde(skip_serializing_if = "Option::is_none")]
pub floating: Option<Vec<MatchingRule>>,
/// Rules to ignore specific windows from the transparency feature
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency_ignore: Option<Vec<MatchingRule>>,
/// Rules to identify applications which minimize to the tray or have multiple windows
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_and_multi_window: Option<Vec<MatchingRule>>,
/// Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style
#[serde(skip_serializing_if = "Option::is_none")]
pub layered: Option<Vec<MatchingRule>>,
/// Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change: Option<Vec<MatchingRule>>,
/// Rules to identify applications which are slow to send initial event notifications
#[serde(skip_serializing_if = "Option::is_none")]
pub slow_application: Option<Vec<MatchingRule>>,
}
impl From<Vec<ApplicationConfiguration>> for ApplicationSpecificConfiguration {
fn from(value: Vec<ApplicationConfiguration>) -> Self {
let mut map = BTreeMap::new();
for entry in &value {
let key = entry.name.clone();
let mut rules = AscApplicationRules {
ignore: None,
manage: None,
floating: None,
transparency_ignore: None,
tray_and_multi_window: None,
layered: None,
object_name_change: None,
slow_application: None,
};
rules.ignore = entry.ignore_identifiers.clone();
if let Some(options) = &entry.options {
for opt in options {
match opt {
ApplicationOptions::ObjectNameChange => {
rules.object_name_change =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::Layered => {
rules.layered =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::TrayAndMultiWindow => {
rules.tray_and_multi_window =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::Force => {
rules.manage =
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
}
ApplicationOptions::BorderOverflow => {}
}
}
}
if rules.ignore.is_some()
|| rules.manage.is_some()
|| rules.floating.is_some()
|| rules.transparency_ignore.is_some()
|| rules.tray_and_multi_window.is_some()
|| rules.layered.is_some()
|| rules.object_name_change.is_some()
|| rules.slow_application.is_some()
{
map.insert(key, AscApplicationRulesOrSchema::AscApplicationRules(rules));
}
}
Self(map)
}
}

View File

@@ -116,7 +116,8 @@ pub struct ApplicationConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<ApplicationOptions>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub float_identifiers: Option<Vec<MatchingRule>>,
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Option<Vec<MatchingRule>>,
}
impl ApplicationConfiguration {
@@ -187,7 +188,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("# Generated by komorebic.exe"), String::new()];
let mut float_rules = vec![];
let mut ignore_rules = vec![];
for app in cfgen {
lines.push(format!("# {}", app.name));
@@ -201,15 +202,15 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_identifiers {
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule =
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("# {comment}"));
@@ -238,7 +239,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("; Generated by komorebic.exe"), String::new()];
let mut float_rules = vec![];
let mut ignore_rules = vec![];
for app in cfgen {
lines.push(format!("; {}", app.name));
@@ -252,8 +253,8 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_identifiers {
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
@@ -261,8 +262,8 @@ impl ApplicationConfigurationGenerator {
);
// Don't want to send duped signals especially as configs get larger
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
if !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("; {comment}"));

View File

@@ -14,6 +14,8 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::animation::prefix::AnimationPrefix;
use crate::KomorebiTheme;
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
@@ -27,6 +29,7 @@ pub use rect::Rect;
pub mod animation;
pub mod arrangement;
pub mod asc;
pub mod config_generation;
pub mod custom_layout;
pub mod cycle_direction;
@@ -47,6 +50,7 @@ pub enum SocketMessage {
StackWindow(OperationDirection),
UnstackWindow,
CycleStack(CycleDirection),
CycleStackIndex(CycleDirection),
FocusStackWindow(usize),
StackAll,
UnstackAll,
@@ -77,6 +81,7 @@ pub enum SocketMessage {
ToggleMonocle,
ToggleMaximize,
ToggleWindowContainerBehaviour,
ToggleFloatOverride,
WindowHidingBehaviour(HidingBehaviour),
ToggleCrossMonitorMoveBehaviour,
CrossMonitorMoveBehaviour(MoveBehaviour),
@@ -90,6 +95,8 @@ pub enum SocketMessage {
CycleLayout(CycleDirection),
ChangeLayoutCustom(PathBuf),
FlipLayout(Axis),
ToggleWorkspaceWindowContainerBehaviour,
ToggleWorkspaceFloatOverride,
// Monitor and Workspace Commands
MonitorIndexPreference(usize, i32, i32, i32, i32),
DisplayIndexPreference(usize, String),
@@ -100,6 +107,7 @@ pub enum SocketMessage {
Stop,
TogglePause,
Retile,
RetileWithResizeDimensions,
QuickSave,
QuickLoad,
Save(PathBuf),
@@ -108,6 +116,7 @@ pub enum SocketMessage {
CycleFocusWorkspace(CycleDirection),
FocusMonitorNumber(usize),
FocusLastWorkspace,
CloseWorkspace,
FocusWorkspaceNumber(usize),
FocusWorkspaceNumbers(usize),
FocusMonitorWorkspaceNumber(usize, usize),
@@ -138,10 +147,11 @@ pub enum SocketMessage {
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
Animation(bool),
AnimationDuration(u64),
Theme(KomorebiTheme),
Animation(bool, Option<AnimationPrefix>),
AnimationDuration(u64, Option<AnimationPrefix>),
AnimationFps(u64),
AnimationStyle(AnimationStyle),
AnimationStyle(AnimationStyle, Option<AnimationPrefix>),
#[serde(alias = "ActiveWindowBorder")]
Border(bool),
#[serde(alias = "ActiveWindowBorderColour")]
@@ -174,7 +184,8 @@ pub enum SocketMessage {
ClearWorkspaceRules(usize, usize),
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
FloatRule(ApplicationIdentifier, String),
#[serde(alias = "FloatRule")]
IgnoreRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
IdentifyTrayApplication(ApplicationIdentifier, String),
@@ -192,6 +203,7 @@ pub enum SocketMessage {
RemoveTitleBar(ApplicationIdentifier, String),
ToggleTitleBars,
AddSubscriberSocket(String),
AddSubscriberSocketWithOptions(String, SubscribeOptions),
RemoveSubscriberSocket(String),
AddSubscriberPipe(String),
RemoveSubscriberPipe(String),
@@ -217,6 +229,12 @@ impl FromStr for SocketMessage {
}
}
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SubscribeOptions {
/// Only emit notifications when the window manager state has changed
pub filter_state_changes: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
pub enum StackbarMode {
Always,
@@ -288,12 +306,15 @@ pub enum BorderImplementation {
ValueEnum,
JsonSchema,
PartialEq,
Eq,
Hash,
)]
pub enum WindowKind {
Single,
Stack,
Monocle,
Unfocused,
Floating,
}
#[derive(
@@ -331,7 +352,16 @@ pub enum ApplicationIdentifier {
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Copy,
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
@@ -340,18 +370,48 @@ pub enum FocusFollowsMouseImplementation {
Windows,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct WindowManagementBehaviour {
/// The current WindowContainerBehaviour to be used
pub current_behaviour: WindowContainerBehaviour,
/// Override of `current_behaviour` to open new windows as floating windows
/// that can be later toggled to tiled, when false it will default to
/// `current_behaviour` again.
pub float_override: bool,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
Default,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
pub enum WindowContainerBehaviour {
/// Create a new container for each new window
#[default]
Create,
/// Append new windows to the focused window container
Append,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
@@ -385,7 +445,16 @@ pub enum HidingBehaviour {
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows

View File

@@ -37,6 +37,12 @@ impl From<Rect> for RECT {
}
}
impl Rect {
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
self.right == rhs.right && self.bottom == rhs.bottom
}
}
impl Rect {
/// decrease the size of self by the padding amount.
pub fn add_padding<T>(&mut self, padding: T)

View File

@@ -1,7 +1,6 @@
#![warn(clippy::all)]
pub mod animation;
pub mod animation_manager;
pub mod border_manager;
pub mod com;
#[macro_use]
@@ -20,6 +19,7 @@ pub mod set_window_position;
pub mod stackbar_manager;
pub mod static_config;
pub mod styles;
pub mod theme_manager;
pub mod transparency_manager;
pub mod window;
pub mod window_manager;
@@ -46,8 +46,6 @@ use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use animation::*;
pub use animation_manager::*;
pub use colour::*;
pub use core::*;
pub use process_command::*;
@@ -139,7 +137,7 @@ lazy_static! {
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
static ref IGNORE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
// mstsc.exe creates these on Windows 11 when a WSL process is launched
// https://github.com/LGUG2Z/komorebi/issues/74
MatchingRule::Simple(IdWithIdentifier {
@@ -158,6 +156,7 @@ lazy_static! {
matching_strategy: Option::from(MatchingStrategy::Equals),
})
]));
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"Chrome_RenderWidgetHostHWND".to_string(),
]));
@@ -176,6 +175,8 @@ lazy_static! {
Arc::new(Mutex::new(HashMap::new()));
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref SUBSCRIPTION_SOCKET_OPTIONS: Arc<Mutex<HashMap<String, SubscribeOptions>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
@@ -212,19 +213,12 @@ lazy_static! {
)
};
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
Arc::new(Mutex::new(AnimationStyle::Linear));
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
// Use app-specific titlebar removal options where possible
// eg. Windows Terminal, IntelliJ IDEA, Firefox
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
@@ -235,8 +229,6 @@ pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
@@ -297,18 +289,38 @@ pub struct Notification {
pub state: State,
}
pub fn notify_subscribers(notification: &str) -> Result<()> {
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
let is_override_event = matches!(
notification.event,
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
| NotificationEvent::Socket(SocketMessage::Theme(_))
| NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(_))
| NotificationEvent::WindowManager(WindowManagerEvent::TitleUpdate(_, _))
| NotificationEvent::WindowManager(WindowManagerEvent::Show(_, _))
);
let notification = &serde_json::to_string(&notification)?;
let mut stale_sockets = vec![];
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
let options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
for (socket, path) in &mut *sockets {
match UnixStream::connect(path) {
Ok(mut stream) => {
tracing::debug!("pushed notification to subscriber: {socket}");
stream.write_all(notification.as_bytes())?;
}
Err(_) => {
stale_sockets.push(socket.clone());
let apply_state_filter = (*options)
.get(socket)
.copied()
.unwrap_or_default()
.filter_state_changes;
if !apply_state_filter || state_has_been_modified || is_override_event {
match UnixStream::connect(path) {
Ok(mut stream) => {
tracing::debug!("pushed notification to subscriber: {socket}");
stream.write_all(notification.as_bytes())?;
}
Err(_) => {
stale_sockets.push(socket.clone());
}
}
}
}
@@ -316,6 +328,13 @@ pub fn notify_subscribers(notification: &str) -> Result<()> {
for socket in stale_sockets {
tracing::warn!("removing stale subscription: {socket}");
sockets.remove(&socket);
let socket_path = DATA_DIR.join(socket);
if let Err(error) = std::fs::remove_file(&socket_path) {
tracing::error!(
"could not remove stale subscriber socket file at {}: {error}",
socket_path.display()
)
}
}
let mut stale_pipes = vec![];

View File

@@ -17,6 +17,9 @@ use std::time::Duration;
use clap::Parser;
use color_eyre::Result;
use crossbeam_utils::Backoff;
use komorebi::animation::AnimationEngine;
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
@@ -38,6 +41,7 @@ use komorebi::process_movement::listen_for_movements;
use komorebi::reaper;
use komorebi::stackbar_manager;
use komorebi::static_config::StaticConfig;
use komorebi::theme_manager;
use komorebi::transparency_manager;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
@@ -271,6 +275,7 @@ fn main() -> Result<()> {
monitor_reconciliator::listen_for_notifications(wm.clone())?;
reaper::watch_for_orphans(wm.clone());
focus_manager::listen_for_notifications(wm.clone());
theme_manager::listen_for_notifications();
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
ctrlc::set_handler(move || {
@@ -285,7 +290,10 @@ fn main() -> Result<()> {
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
wm.lock().restore_all_windows()?;
AnimationEngine::wait_for_all_animations();
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;

View File

@@ -20,6 +20,7 @@ use crate::workspace::Workspace;
use crate::DefaultLayout;
use crate::Layout;
use crate::OperationDirection;
use crate::WindowsApi;
#[derive(
Debug,
@@ -138,6 +139,86 @@ impl Monitor {
Ok(())
}
/// Adds a container to this `Monitor` using the move direction to calculate if the container
/// should be added in front of all containers, in the back or in place of the focused
/// container, moving the rest along. The move direction should be from the origin monitor
/// towards the target monitor or from the origin workspace towards the target workspace.
pub fn add_container_with_direction(
&mut self,
container: Container,
workspace_idx: Option<usize>,
direction: OperationDirection,
) -> Result<()> {
let workspace = if let Some(idx) = workspace_idx {
self.workspaces_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
} else {
self.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?
};
match direction {
OperationDirection::Left => {
// insert the container into the workspace on the monitor at the back (or rightmost position)
// if we are moving across a boundary to the left (back = right side of the target)
match workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.insert_container_at_idx(0, container);
} else {
workspace.add_container_to_back(container);
}
}
_ => {
workspace.add_container_to_back(container);
}
},
Layout::Custom(_) => {
workspace.add_container_to_back(container);
}
}
}
OperationDirection::Right => {
// insert the container into the workspace on the monitor at the front (or leftmost position)
// if we are moving across a boundary to the right (front = left side of the target)
match workspace.layout() {
Layout::Default(layout) => {
let target_index = layout.leftmost_index(workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.add_container_to_back(container);
} else {
workspace.insert_container_at_idx(target_index, container);
}
}
_ => {
workspace.insert_container_at_idx(target_index, container);
}
}
}
Layout::Custom(_) => {
workspace.add_container_to_front(container);
}
}
}
OperationDirection::Up | OperationDirection::Down => {
// insert the container into the workspace on the monitor at the position
// where the currently focused container on that workspace is
workspace.insert_container_at_idx(workspace.focused_container_idx(), container);
}
};
Ok(())
}
pub fn remove_workspace_by_idx(&mut self, idx: usize) -> Option<Workspace> {
if idx < self.workspaces().len() {
return self.workspaces_mut().remove(idx);
@@ -178,65 +259,49 @@ impl Monitor {
bail!("cannot move native maximized window to another monitor or workspace");
}
let container = workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
let foreground_hwnd = WindowsApi::foreground_window()?;
let floating_window_index = workspace
.floating_windows()
.iter()
.position(|w| w.hwnd == foreground_hwnd);
let workspaces = self.workspaces_mut();
if let Some(idx) = floating_window_index {
let window = workspace.floating_windows_mut().remove(idx);
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
Some(workspace) => workspace,
};
match direction {
Some(OperationDirection::Left) => match target_workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
target_workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.insert_container_at_idx(0, container);
} else {
target_workspace.add_container_to_back(container);
}
}
_ => {
target_workspace.add_container_to_back(container);
}
},
Layout::Custom(_) => {
target_workspace.add_container_to_back(container);
let workspaces = self.workspaces_mut();
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
},
Some(OperationDirection::Right) => match target_workspace.layout() {
Layout::Default(layout) => {
let target_index = layout.leftmost_index(target_workspace.containers().len());
Some(workspace) => workspace,
};
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.add_container_to_back(container);
} else {
target_workspace.insert_container_at_idx(target_index, container);
}
}
_ => {
target_workspace.insert_container_at_idx(target_index, container);
}
}
target_workspace.floating_windows_mut().push(window);
} else {
let container = workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
let workspaces = self.workspaces_mut();
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
Layout::Custom(_) => {
target_workspace.add_container_to_front(container);
}
},
_ => {
Some(workspace) => workspace,
};
if let Some(direction) = direction {
self.add_container_with_direction(
container,
Some(target_workspace_idx),
direction,
)?;
} else {
target_workspace.add_container_to_back(container);
}
}

View File

@@ -132,6 +132,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
);
ACTIVE.store(true, Ordering::SeqCst);
border_manager::send_notification(None);
}
continue 'receiver;
@@ -172,7 +173,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if should_update {
tracing::info!("updated work area for {}", monitor.device_id());
monitor.update_focused_workspace(offset)?;
border_manager::send_notification();
border_manager::send_notification(None);
} else {
tracing::debug!(
"work areas match, reconciliation not required for {}",
@@ -219,7 +220,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
);
monitor.update_focused_workspace(offset)?;
border_manager::send_notification();
border_manager::send_notification(None);
} else {
tracing::debug!(
"resolutions match, reconciliation not required for {}",
@@ -406,7 +407,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Second retile to fix DPI/resolution related jank
wm.retile_all(true)?;
// Border updates to fix DPI/resolution related jank
border_manager::send_notification();
border_manager::send_notification(None);
}
}
}

View File

@@ -22,6 +22,10 @@ use schemars::gen::SchemaSettings;
use schemars::schema_for;
use uds_windows::UnixStream;
use crate::animation::AnimationEngine;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
@@ -40,6 +44,10 @@ use crate::core::StateQuery;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowKind;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
@@ -51,6 +59,7 @@ use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
use crate::static_config::StaticConfig;
use crate::theme_manager;
use crate::transparency_manager;
use crate::window::RuleDebug;
use crate::window::Window;
@@ -61,15 +70,12 @@ use crate::winevent_listener;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
use crate::State;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
@@ -79,6 +85,7 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
use crate::TCP_CONNECTIONS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
@@ -106,10 +113,19 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
tracing::info!("listening on komorebi.sock");
for client in listener.incoming() {
match client {
Ok(stream) => match read_commands_uds(&wm, stream) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
},
Ok(stream) => {
let wm_clone = wm.clone();
std::thread::spawn(move || {
match stream.set_read_timeout(Some(Duration::from_secs(1))) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
}
match read_commands_uds(&wm_clone, stream) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
}
});
}
Err(error) => {
tracing::error!("{}", error);
break;
@@ -187,6 +203,10 @@ impl WindowManager {
}
}
#[allow(clippy::useless_asref)]
// We don't have From implemented for &mut WindowManager
let initial_state = State::from(self.as_ref());
match message {
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
if let Some(monitor) = self.focused_monitor_mut() {
@@ -231,7 +251,18 @@ impl WindowManager {
self.cycle_container_window_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::CycleStackIndex(direction) => {
self.cycle_container_window_index_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::FocusStackWindow(idx) => {
// In case you are using this command on a bar on a monitor
// different from the currently focused one, you'd want that
// monitor to be focused so that the FocusStackWindow happens
// on the monitor with the bar you just pressed.
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
}
self.focus_container_window(idx)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
@@ -387,20 +418,20 @@ impl WindowManager {
}));
}
}
SocketMessage::FloatRule(identifier, ref id) => {
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
SocketMessage::IgnoreRule(identifier, ref id) => {
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let mut should_push = true;
for f in &*float_identifiers {
if let MatchingRule::Simple(f) = f {
if f.id.eq(id) {
for i in &*ignore_identifiers {
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
ignore_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
@@ -505,7 +536,8 @@ impl WindowManager {
self.move_container_to_workspace(workspace_idx, true, None)?;
}
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
}
SocketMessage::SwapWorkspacesToMonitorNumber(monitor_idx) => {
self.swap_focused_monitor(monitor_idx)?;
@@ -517,7 +549,8 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.move_container_to_monitor(monitor_idx, None, true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
}
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, false, None)?;
@@ -539,7 +572,8 @@ impl WindowManager {
self.move_container_to_workspace(workspace_idx, false, None)?;
}
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, false)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
}
SocketMessage::CycleSendContainerToMonitor(direction) => {
let monitor_idx = direction.next_idx(
@@ -548,22 +582,37 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.move_container_to_monitor(monitor_idx, None, false)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
}
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
false,
direction,
)?;
}
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
true,
direction,
)?;
}
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
false,
direction,
)?;
}
}
@@ -571,7 +620,13 @@ impl WindowManager {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
true,
direction,
)?;
}
}
@@ -615,10 +670,13 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::Retile => {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
border_manager::destroy_all_borders()?;
self.retile_all(false)?
}
SocketMessage::RetileWithResizeDimensions => {
border_manager::destroy_all_borders()?;
self.retile_all(true)?
}
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
@@ -742,6 +800,44 @@ impl WindowManager {
self.focus_workspace(workspace_idx)?;
}
SocketMessage::CloseWorkspace => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
self.focus_monitor(monitor_idx)?;
}
let mut can_close = false;
if let Some(monitor) = self.focused_monitor_mut() {
let focused_workspace_idx = monitor.focused_workspace_idx();
let last_focused_workspace = monitor
.last_focused_workspace()
.unwrap_or(focused_workspace_idx.saturating_sub(1));
if let Some(workspace) = monitor.focused_workspace() {
if monitor.workspaces().len() > 1
&& workspace.containers().is_empty()
&& workspace.floating_windows().is_empty()
&& workspace.monocle_container().is_none()
&& workspace.maximized_window().is_none()
&& workspace.name().is_none()
{
can_close = true;
}
}
if can_close
&& monitor
.workspaces_mut()
.remove(focused_workspace_idx)
.is_some()
{
self.focus_workspace(last_focused_workspace)?;
}
}
}
SocketMessage::FocusLastWorkspace => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
@@ -819,7 +915,11 @@ impl WindowManager {
tracing::info!(
"received stop command, restoring all hidden windows and terminating process"
);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
self.restore_all_windows()?;
AnimationEngine::wait_for_all_animations();
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
@@ -1312,6 +1412,14 @@ impl WindowManager {
let socket_path = DATA_DIR.join(socket);
sockets.insert(socket.clone(), socket_path);
}
SocketMessage::AddSubscriberSocketWithOptions(ref socket, options) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
let socket_path = DATA_DIR.join(socket);
sockets.insert(socket.clone(), socket_path);
let mut socket_options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
socket_options.insert(socket.clone(), options);
}
SocketMessage::RemoveSubscriberSocket(ref socket) => {
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
sockets.remove(socket);
@@ -1339,15 +1447,52 @@ impl WindowManager {
self.resize_delta = delta;
}
SocketMessage::ToggleWindowContainerBehaviour => {
match self.window_container_behaviour {
match self.window_management_behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
self.window_container_behaviour = WindowContainerBehaviour::Append;
self.window_management_behaviour.current_behaviour =
WindowContainerBehaviour::Append;
}
WindowContainerBehaviour::Append => {
self.window_container_behaviour = WindowContainerBehaviour::Create;
self.window_management_behaviour.current_behaviour =
WindowContainerBehaviour::Create;
}
}
}
SocketMessage::ToggleFloatOverride => {
self.window_management_behaviour.float_override =
!self.window_management_behaviour.float_override;
}
SocketMessage::ToggleWorkspaceWindowContainerBehaviour => {
let current_global_behaviour = self.window_management_behaviour.current_behaviour;
if let Some(behaviour) = self
.focused_workspace_mut()?
.window_container_behaviour_mut()
{
match behaviour {
WindowContainerBehaviour::Create => {
*behaviour = WindowContainerBehaviour::Append
}
WindowContainerBehaviour::Append => {
*behaviour = WindowContainerBehaviour::Create
}
}
} else {
self.focused_workspace_mut()?
.set_window_container_behaviour(Some(match current_global_behaviour {
WindowContainerBehaviour::Create => WindowContainerBehaviour::Append,
WindowContainerBehaviour::Append => WindowContainerBehaviour::Create,
}));
};
}
SocketMessage::ToggleWorkspaceFloatOverride => {
let current_global_override = self.window_management_behaviour.float_override;
if let Some(float_override) = self.focused_workspace_mut()?.float_override_mut() {
*float_override = !*float_override;
} else {
self.focused_workspace_mut()?
.set_float_override(Some(!current_global_override));
};
}
SocketMessage::WindowHidingBehaviour(behaviour) => {
let mut hiding_behaviour = HIDING_BEHAVIOUR.lock();
*hiding_behaviour = behaviour;
@@ -1371,6 +1516,16 @@ impl WindowManager {
}
SocketMessage::Border(enable) => {
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
if !enable {
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
border_manager::destroy_all_borders()?;
}
BorderImplementation::Windows => {
self.remove_all_accents()?;
}
}
}
}
SocketMessage::BorderImplementation(implementation) => {
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
@@ -1388,7 +1543,7 @@ impl WindowManager {
}
}
border_manager::send_notification();
border_manager::send_notification(None);
}
}
SocketMessage::BorderColour(kind, r, g, b) => match kind {
@@ -1404,6 +1559,9 @@ impl WindowManager {
WindowKind::Unfocused => {
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Floating => {
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
},
SocketMessage::BorderStyle(style) => {
STYLE.store(style);
@@ -1414,18 +1572,41 @@ impl WindowManager {
SocketMessage::BorderOffset(offset) => {
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
}
SocketMessage::Animation(enable) => {
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::AnimationDuration(duration) => {
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
}
SocketMessage::Animation(enable, prefix) => match prefix {
Some(prefix) => {
ANIMATION_ENABLED_PER_ANIMATION
.lock()
.insert(prefix, enable);
}
None => {
ANIMATION_ENABLED_GLOBAL.store(enable, Ordering::SeqCst);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
}
},
SocketMessage::AnimationDuration(duration, prefix) => match prefix {
Some(prefix) => {
ANIMATION_DURATION_PER_ANIMATION
.lock()
.insert(prefix, duration);
}
None => {
ANIMATION_DURATION_GLOBAL.store(duration, Ordering::SeqCst);
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
}
},
SocketMessage::AnimationFps(fps) => {
ANIMATION_FPS.store(fps, Ordering::SeqCst);
}
SocketMessage::AnimationStyle(style) => {
*ANIMATION_STYLE.lock() = style;
}
SocketMessage::AnimationStyle(style, prefix) => match prefix {
Some(prefix) => {
ANIMATION_STYLE_PER_ANIMATION.lock().insert(prefix, style);
}
None => {
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
*animation_style = style;
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
}
},
SocketMessage::ToggleTransparency => {
let current = transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED.store(!current, Ordering::SeqCst);
@@ -1522,18 +1703,23 @@ impl WindowManager {
reply.write_all(schema.as_bytes())?;
}
SocketMessage::Theme(theme) => {
theme_manager::send_notification(theme);
}
// Deprecated commands
SocketMessage::AltFocusHack(_)
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
};
let notification = Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
};
notify_subscribers(
Notification {
event: NotificationEvent::Socket(message.clone()),
state: self.as_ref().into(),
},
initial_state.has_been_modified(self.as_ref()),
)?;
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification();
border_manager::send_notification(None);
transparency_manager::send_notification();
stackbar_manager::send_notification();
@@ -1551,22 +1737,29 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
for line in reader.lines() {
let message = SocketMessage::from_str(&line?)?;
let mut wm = wm.lock();
if wm.is_paused {
return match message {
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())
match wm.try_lock_for(Duration::from_secs(1)) {
None => {
tracing::warn!(
"could not acquire window manager lock, not processing message: {message}"
);
}
Some(mut wm) => {
if wm.is_paused {
return match message {
SocketMessage::TogglePause
| SocketMessage::State
| SocketMessage::GlobalState
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
_ => {
tracing::trace!("ignoring while paused");
Ok(())
}
};
}
};
}
wm.process_command(message.clone(), &mut stream)?;
wm.process_command(message.clone(), &mut stream)?;
}
}
}
Ok(())

View File

@@ -32,7 +32,9 @@ use crate::workspace_reconciliator::ALT_TAB_HWND;
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;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
@@ -101,6 +103,10 @@ impl WindowManager {
}
if !transparency_override {
if rule_debug.matches_ignore_identifier.is_some() {
border_manager::send_notification(Option::from(event.hwnd()));
}
return Ok(());
}
}
@@ -117,6 +123,10 @@ impl WindowManager {
}
}
#[allow(clippy::useless_asref)]
// We don't have From implemented for &mut WindowManager
let initial_state = State::from(self.as_ref());
// Make sure we have the most recently focused monitor from any event
match event {
WindowManagerEvent::FocusChange(_, window)
@@ -149,14 +159,6 @@ impl WindowManager {
_ => {}
}
for monitor in self.monitors_mut() {
for workspace in monitor.workspaces_mut() {
if let WindowManagerEvent::FocusChange(_, window) = event {
let _ = workspace.focus_changed(window.hwnd);
}
}
}
self.enforce_workspace_rules()?;
if matches!(event, WindowManagerEvent::MouseCapture(..)) {
@@ -226,8 +228,7 @@ impl WindowManager {
.is_some();
if !window.is_window()
|| should_act
|| !programmatically_hidden_hwnds.contains(&window.hwnd)
|| (should_act && !programmatically_hidden_hwnds.contains(&window.hwnd))
{
hide = true;
}
@@ -246,155 +247,221 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, false)?;
let workspace = self.focused_workspace_mut()?;
if !workspace
let floating_window_idx = workspace
.floating_windows()
.iter()
.any(|w| w.hwnd == window.hwnd)
{
if let Some(w) = workspace.maximized_window() {
if w.hwnd == window.hwnd {
return Ok(());
.position(|w| w.hwnd == window.hwnd);
match floating_window_idx {
None => {
if let Some(w) = workspace.maximized_window() {
if w.hwnd == window.hwnd {
return Ok(());
}
}
if let Some(monocle) = workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(false)?;
}
} else {
workspace.focus_container_by_window(window.hwnd)?;
}
}
if let Some(monocle) = workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
Some(idx) => {
if let Some(window) = workspace.floating_windows().get(idx) {
window.focus(false)?;
}
} else {
self.focused_workspace_mut()?
.focus_container_by_window(window.hwnd)?;
}
}
}
WindowManagerEvent::Show(_, window)
| WindowManagerEvent::Manage(window)
| WindowManagerEvent::Uncloak(_, window) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx =
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
if matches!(event, WindowManagerEvent::Uncloak(_, _))
&& self.uncloack_to_ignore >= 1
{
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
} else {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx =
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let mut needs_reconciliation = false;
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())
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();
}
}
}
workspace_reconciliator::send_notification(i, j);
needs_reconciliation = true;
}
}
}
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd)
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
);
window.hide();
proceed = false;
}
}
}
if proceed {
let mut behaviour = self.window_management_behaviour(
focused_monitor_idx,
focused_workspace_idx,
);
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && !needs_reconciliation {
let floating_applications = FLOATING_APPLICATIONS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut should_float = false;
if !floating_applications.is_empty() {
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
(window.title(), window.exe(), window.class(), window.path())
{
// 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();
should_float = should_act(
&title,
&exe_name,
&class,
&path,
&floating_applications,
&regex_identifiers,
)
.is_some();
}
}
behaviour.float_override = behaviour.float_override
|| (should_float
&& !matches!(event, WindowManagerEvent::Manage(_)));
if behaviour.float_override {
workspace.floating_windows_mut().push(window);
self.update_focused_workspace(false, false)?;
} else {
match behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| {
anyhow!("there is no focused container")
})?
.add_window(window);
self.update_focused_workspace(true, false)?;
stackbar_manager::send_notification();
}
}
}
workspace_reconciliator::send_notification(i, j);
needs_reconciliation = true;
}
}
}
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.container_for_window(window.hwnd).is_some()
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
);
window.hide();
proceed = false;
}
}
}
if proceed {
let behaviour =
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && !needs_reconciliation {
match behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
stackbar_manager::send_notification();
if (self.focused_workspace()?.containers().len() == 1
&& self.focused_workspace()?.floating_windows().is_empty())
|| (self.focused_workspace()?.containers().is_empty()
&& self.focused_workspace()?.floating_windows().len() == 1)
{
// If after adding this window the workspace only contains 1 window, it
// means it was previously empty and we focused the desktop to unfocus
// any previous window from other workspace, so now we need to focus
// this window again. This is needed because sometimes some windows
// first send the `FocusChange` event and only the `Show` event after
// and we will be focusing the desktop on the `FocusChange` event since
// it is still empty.
window.focus(self.mouse_follows_focus)?;
}
}
}
if workspace_contains_window {
let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container {
if let Some(monocle_window) = monocle.focused_window() {
if monocle_window.hwnd == window.hwnd {
monocle_window_event = true;
if workspace_contains_window {
let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container {
if let Some(monocle_window) = monocle.focused_window() {
if monocle_window.hwnd == window.hwnd {
monocle_window_event = true;
}
}
}
}
if !monocle_window_event && monocle_container.is_some() {
window.hide();
if !monocle_window_event && monocle_container.is_some() {
window.hide();
}
}
}
}
}
WindowManagerEvent::MoveResizeStart(_, window) => {
if *self.focused_workspace()?.tile() {
let monitor_idx = self.focused_monitor_idx();
let workspace_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace_idx();
let container_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
.focused_container_idx();
let monitor_idx = self.focused_monitor_idx();
let workspace_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace_idx();
WindowsApi::bring_window_to_top(window.hwnd)?;
WindowsApi::bring_window_to_top(window.hwnd)?;
self.pending_move_op =
Option::from((monitor_idx, workspace_idx, container_idx));
}
let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
*pending_move_op = Option::from((monitor_idx, workspace_idx, window.hwnd));
}
WindowManagerEvent::MoveResizeEnd(_, window) => {
// We need this because if the event ends on a different monitor,
// that monitor will already have been focused and updated in the state
let pending = self.pending_move_op;
let pending = *self.pending_move_op;
// Always consume the pending move op whenever this event is handled
self.pending_move_op = None;
let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
*pending_move_op = None;
// If the window handles don't match then something went wrong and the pending move
// is not related to this current move, if so abort this operation.
if let Some((_, _, w_hwnd)) = pending {
if w_hwnd != window.hwnd {
color_eyre::eyre::bail!(
"window handles for move operation don't match: {} != {}",
w_hwnd,
window.hwnd
);
}
}
let target_monitor_idx = self
.monitor_idx_from_current_pos()
@@ -402,8 +469,8 @@ impl WindowManager {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
let window_container_behaviour =
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
let window_management_behaviour =
self.window_management_behaviour(focused_monitor_idx, focused_workspace_idx);
let workspace = self.focused_workspace_mut()?;
let focused_container_idx = workspace.focused_container_idx();
@@ -418,8 +485,21 @@ impl WindowManager {
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
// 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((origin_monitor_idx, origin_workspace_idx, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
@@ -440,8 +520,7 @@ impl WindowManager {
.get(origin_workspace_idx)
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
let managed_window =
origin_workspace.contains_managed_window(window.hwnd);
let managed_window = origin_workspace.contains_window(window.hwnd);
if !managed_window {
moved_across_monitors = false;
@@ -451,7 +530,9 @@ impl WindowManager {
}
let workspace = self.focused_workspace_mut()?;
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
if (*workspace.tile() && workspace.contains_managed_window(window.hwnd))
|| moved_across_monitors
{
let resize = Rect {
left: new_position.left - old_position.left,
top: new_position.top - old_position.top,
@@ -471,11 +552,8 @@ impl WindowManager {
tracing::info!("moving with mouse");
if moved_across_monitors {
if let Some((
origin_monitor_idx,
origin_workspace_idx,
origin_container_idx,
)) = pending
if let Some((origin_monitor_idx, origin_workspace_idx, w_hwnd)) =
pending
{
let target_workspace_idx = self
.monitors()
@@ -495,34 +573,41 @@ impl WindowManager {
// Default to 0 in the case of an empty workspace
.unwrap_or(0);
self.transfer_container(
(
origin_monitor_idx,
origin_workspace_idx,
origin_container_idx,
),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
let origin = (origin_monitor_idx, origin_workspace_idx, w_hwnd);
let target = (
target_monitor_idx,
target_workspace_idx,
target_container_idx,
);
self.transfer_window(origin, target)?;
// We want to make sure both the origin and target monitors are updated,
// so that we don't have ghost tiles until we force an interaction on
// the origin monitor's focused workspace
self.focus_monitor(origin_monitor_idx)?;
self.focus_workspace(origin_workspace_idx)?;
let origin_monitor = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
origin_monitor.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false, false)?;
self.focus_monitor(target_monitor_idx)?;
self.focus_workspace(target_workspace_idx)?;
let target_monitor = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
target_monitor.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false, false)?;
// Make sure to give focus to the moved window again
window.focus(self.mouse_follows_focus)?;
}
// Here we handle a simple move on the same monitor which is treated as
// a container swap
} else if window_management_behaviour.float_override {
workspace.floating_windows_mut().push(window);
self.update_focused_workspace(false, false)?;
} else {
match window_container_behaviour {
match window_management_behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
match workspace.container_idx_from_current_point() {
Some(target_idx) => {
@@ -624,6 +709,20 @@ impl WindowManager {
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);
}
}
}
}
@@ -636,13 +735,15 @@ impl WindowManager {
serde_json::to_writer_pretty(&file, &known_hwnds)?;
let notification = Notification {
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
};
notify_subscribers(
Notification {
event: NotificationEvent::WindowManager(event),
state: self.as_ref().into(),
},
initial_state.has_been_modified(self.as_ref()),
)?;
notify_subscribers(&serde_json::to_string(&notification)?)?;
border_manager::send_notification();
border_manager::send_notification(Some(event.hwnd()));
transparency_manager::send_notification();
stackbar_manager::send_notification();

View File

@@ -51,7 +51,7 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
let reaped_orphans = workspace.reap_orphans()?;
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
workspace.update(&work_area, offset, window_based_work_area_offset)?;
border_manager::send_notification();
border_manager::send_notification(None);
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,

View File

@@ -1,8 +1,16 @@
use crate::animation::PerAnimationPrefixConfig;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::animation::DEFAULT_ANIMATION_FPS;
use crate::border_manager;
use crate::border_manager::ZOrder;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::colour::Colour;
use crate::core::BorderImplementation;
use crate::core::StackbarLabel;
@@ -20,6 +28,7 @@ use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::theme_manager;
use crate::transparency_manager;
use crate::window;
use crate::window_manager::WindowManager;
@@ -27,16 +36,13 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::CrossBoundaryBehaviour;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::FLOATING_APPLICATIONS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
@@ -49,6 +55,8 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
use crate::WORKSPACE_MATCHING_RULES;
use crate::asc::ApplicationSpecificConfiguration;
use crate::asc::AscApplicationRulesOrSchema;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::ApplicationConfigurationGenerator;
@@ -67,6 +75,7 @@ use crate::core::OperationBehaviour;
use crate::core::Rect;
use crate::core::SocketMessage;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::EventKind;
@@ -94,6 +103,8 @@ pub struct BorderColours {
pub stack: Option<Colour>,
/// Border colour when the container is in monocle mode
pub monocle: Option<Colour>,
/// Border colour when the container is in floating mode
pub floating: Option<Colour>,
/// Border colour when the container is unfocused
pub unfocused: Option<Colour>,
}
@@ -105,13 +116,13 @@ pub struct WorkspaceConfig {
/// Layout (default: BSP)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<DefaultLayout>,
/// Custom Layout (default: None)
/// END OF LIFE FEATURE: Custom Layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_layout: Option<PathBuf>,
/// Layout rules (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
/// Layout rules (default: None)
/// END OF LIFE FEATURE: Custom layout rules (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_layout_rules: Option<HashMap<usize, PathBuf>>,
/// Container padding (default: global)
@@ -129,6 +140,13 @@ pub struct WorkspaceConfig {
/// Apply this monitor's window-based work area offset (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub apply_window_based_work_area_offset: Option<bool>,
/// 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)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_override: Option<bool>,
}
impl From<&Workspace> for WorkspaceConfig {
@@ -181,6 +199,8 @@ impl From<&Workspace> for WorkspaceConfig {
initial_workspace_rules: None,
workspace_rules: None,
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
window_container_behaviour: *value.window_container_behaviour(),
float_override: *value.float_override(),
}
}
}
@@ -217,7 +237,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.30`
/// The `komorebi.json` static configuration file reference for `v0.1.31`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -234,6 +254,10 @@ pub struct StaticConfig {
/// 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)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_override: Option<bool>,
/// Determine what happens when a window is moved across a monitor boundary (default: Swap)
#[serde(skip_serializing_if = "Option::is_none")]
pub cross_monitor_move_behaviour: Option<MoveBehaviour>,
@@ -243,13 +267,13 @@ pub struct StaticConfig {
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
#[serde(skip_serializing_if = "Option::is_none")]
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
/// Determine focus follows mouse implementation (default: None)
/// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead
#[serde(skip_serializing_if = "Option::is_none")]
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
/// Enable or disable mouse follows focus (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub mouse_follows_focus: Option<bool>,
/// Path to applications.yaml from komorebi-application-specific-configurations (default: None)
/// Path to applications.json from komorebi-application-specific-configurations (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub app_specific_configuration_path: Option<PathBuf>,
/// Width of the window border (default: 8)
@@ -272,7 +296,7 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_style")]
pub border_style: Option<BorderStyle>,
/// Active window border z-order (default: System)
/// DEPRECATED from v0.1.31: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>,
/// Active window border implementation (default: Komorebi)
@@ -304,10 +328,14 @@ pub struct StaticConfig {
pub global_work_area_offset: Option<Rect>,
/// Individual window floating rules
#[serde(skip_serializing_if = "Option::is_none")]
pub float_rules: Option<Vec<MatchingRule>>,
#[serde(alias = "float_rules")]
pub ignore_rules: Option<Vec<MatchingRule>>,
/// Individual window force-manage rules
#[serde(skip_serializing_if = "Option::is_none")]
pub manage_rules: Option<Vec<MatchingRule>>,
/// Identify applications which should be managed as floating windows
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_applications: Option<Vec<MatchingRule>>,
/// Identify border overflow applications
#[serde(skip_serializing_if = "Option::is_none")]
pub border_overflow_applications: Option<Vec<MatchingRule>>,
@@ -341,16 +369,20 @@ pub struct StaticConfig {
/// How long to wait when compensating for slow applications, in milliseconds (default: 20)
#[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
pub bar_configurations: Option<Vec<PathBuf>>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct AnimationsConfig {
/// Enable or disable animations (default: false)
enabled: bool,
enabled: PerAnimationPrefixConfig<bool>,
/// Set the animation duration in ms (default: 250)
duration: Option<u64>,
duration: Option<PerAnimationPrefixConfig<u64>>,
/// Set the animation style (default: Linear)
style: Option<AnimationStyle>,
style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
/// Set the animation FPS (default: 60)
fps: Option<u64>,
}
@@ -359,7 +391,7 @@ pub struct AnimationsConfig {
pub enum KomorebiTheme {
/// A theme from catppuccin-egui
Catppuccin {
/// Name of the Catppuccin theme
/// 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)
single_border: Option<komorebi_themes::CatppuccinValue>,
@@ -367,6 +399,8 @@ pub enum KomorebiTheme {
stack_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is in monocle mode (default: Pink)
monocle_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the window is floating (default: Yellow)
floating_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused (default: Base)
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar focused tab text colour (default: Green)
@@ -380,7 +414,7 @@ pub enum KomorebiTheme {
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
name: komorebi_themes::Base16,
/// Border colour when the container contains a single window (default: Base0D)
single_border: Option<komorebi_themes::Base16Value>,
@@ -388,6 +422,8 @@ pub enum KomorebiTheme {
stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
@@ -402,6 +438,31 @@ pub enum KomorebiTheme {
}
impl StaticConfig {
pub fn end_of_life(raw: &str) {
let features = vec![
"focus_follows_mouse",
"custom_layout",
"custom_layout_rules",
];
let mut display = false;
for feature in features {
if raw.contains(feature) {
if !display {
display = true;
println!("\n\"{feature}\" is now end-of-life");
} else {
println!(r#""{feature}" is now end-of-life"#);
}
}
}
if display {
println!("\nEnd-of-life features will not receive any further bug fixes or updates; they should not be used\n")
}
}
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("border", ["active_window_border"]);
@@ -409,6 +470,8 @@ impl StaticConfig {
map.insert("border_offset", ["active_window_border_offset"]);
map.insert("border_colours", ["active_window_border_colours"]);
map.insert("border_style", ["active_window_border_style"]);
map.insert("applications.json", ["applications.yaml"]);
map.insert("ignore_rules", ["float_rules"]);
let mut display = false;
@@ -433,7 +496,7 @@ impl StaticConfig {
}
pub fn deprecated(raw: &str) {
let deprecated_options = ["invisible_borders"];
let deprecated_options = ["invisible_borders", "border_z_order"];
let deprecated_variants = vec![
("Hide", "window_hiding_behaviour", "Cloak"),
("Minimize", "window_hiding_behaviour", "Cloak"),
@@ -498,6 +561,9 @@ impl From<&WindowManager> for StaticConfig {
single: Option::from(Colour::from(border_manager::FOCUSED.load(Ordering::SeqCst))),
stack: Option::from(Colour::from(border_manager::STACK.load(Ordering::SeqCst))),
monocle: Option::from(Colour::from(border_manager::MONOCLE.load(Ordering::SeqCst))),
floating: Option::from(Colour::from(
border_manager::FLOATING.load(Ordering::SeqCst),
)),
unfocused: Option::from(Colour::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
)),
@@ -507,7 +573,10 @@ impl From<&WindowManager> for StaticConfig {
Self {
invisible_borders: None,
resize_delta: Option::from(value.resize_delta),
window_container_behaviour: Option::from(value.window_container_behaviour),
window_container_behaviour: Option::from(
value.window_management_behaviour.current_behaviour,
),
float_override: Option::from(value.window_management_behaviour.float_override),
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
cross_boundary_behaviour: Option::from(value.cross_boundary_behaviour),
unmanaged_window_operation_behaviour: Option::from(
@@ -530,7 +599,7 @@ impl From<&WindowManager> for StaticConfig {
),
transparency_ignore_rules: None,
border_style: Option::from(STYLE.load()),
border_z_order: Option::from(Z_ORDER.load()),
border_z_order: None,
border_implementation: Option::from(IMPLEMENTATION.load()),
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
@@ -541,7 +610,8 @@ impl From<&WindowManager> for StaticConfig {
monitors: Option::from(monitors),
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
global_work_area_offset: value.work_area_offset,
float_rules: None,
ignore_rules: None,
floating_applications: None,
manage_rules: None,
border_overflow_applications: None,
tray_and_multi_window_applications: None,
@@ -556,6 +626,7 @@ impl From<&WindowManager> for StaticConfig {
SLOW_APPLICATION_COMPENSATION_TIME.load(Ordering::SeqCst),
),
slow_application_identifiers: Option::from(SLOW_APPLICATION_IDENTIFIERS.lock().clone()),
bar_configurations: None,
}
}
}
@@ -587,11 +658,43 @@ impl StaticConfig {
}
if let Some(animations) = &self.animation {
ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst);
ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst);
ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst);
let mut animation_style = ANIMATION_STYLE.lock();
*animation_style = animations.style.unwrap_or(AnimationStyle::Linear);
match &animations.enabled {
PerAnimationPrefixConfig::Prefix(enabled) => {
ANIMATION_ENABLED_PER_ANIMATION.lock().clone_from(enabled);
}
PerAnimationPrefixConfig::Global(enabled) => {
ANIMATION_ENABLED_GLOBAL.store(*enabled, Ordering::SeqCst);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
}
}
match &animations.style {
Some(PerAnimationPrefixConfig::Prefix(style)) => {
ANIMATION_STYLE_PER_ANIMATION.lock().clone_from(style);
}
Some(PerAnimationPrefixConfig::Global(style)) => {
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
*animation_style = *style;
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
}
None => {}
}
match &animations.duration {
Some(PerAnimationPrefixConfig::Prefix(duration)) => {
ANIMATION_DURATION_PER_ANIMATION.lock().clone_from(duration);
}
Some(PerAnimationPrefixConfig::Global(duration)) => {
ANIMATION_DURATION_GLOBAL.store(*duration, Ordering::SeqCst);
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
}
None => {}
}
ANIMATION_FPS.store(
animations.fps.unwrap_or(DEFAULT_ANIMATION_FPS),
Ordering::SeqCst,
);
}
if let Some(container) = self.default_container_padding {
@@ -622,6 +725,10 @@ impl StaticConfig {
border_manager::MONOCLE.store(u32::from(monocle), Ordering::SeqCst);
}
if let Some(floating) = colours.floating {
border_manager::FLOATING.store(u32::from(floating), Ordering::SeqCst);
}
if let Some(unfocused) = colours.unfocused {
border_manager::UNFOCUSED.store(u32::from(unfocused), Ordering::SeqCst);
}
@@ -649,7 +756,7 @@ impl StaticConfig {
}
}
border_manager::send_notification();
border_manager::send_notification(None);
}
transparency_manager::TRANSPARENCY_ENABLED
@@ -657,7 +764,7 @@ impl StaticConfig {
transparency_manager::TRANSPARENCY_ALPHA
.store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst);
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
@@ -665,9 +772,14 @@ impl StaticConfig {
let mut layered_identifiers = LAYERED_WHITELIST.lock();
let mut transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
let mut slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
let mut floating_applications = FLOATING_APPLICATIONS.lock();
if let Some(rules) = &mut self.float_rules {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
if let Some(rules) = &mut self.ignore_rules {
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
}
if let Some(rules) = &mut self.floating_applications {
populate_rules(rules, &mut floating_applications, &mut regex_identifiers)?;
}
if let Some(rules) = &mut self.manage_rules {
@@ -743,184 +855,144 @@ impl StaticConfig {
}
if let Some(theme) = &self.theme {
let (
single_border,
stack_border,
monocle_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
) = match theme {
KomorebiTheme::Catppuccin {
name,
single_border,
stack_border,
monocle_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
..
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
.color32(name.as_theme());
let stack_border = stack_border
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
.color32(name.as_theme());
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::CatppuccinValue::Pink)
.color32(name.as_theme());
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
.color32(name.as_theme());
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
.color32(name.as_theme());
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::CatppuccinValue::Text)
.color32(name.as_theme());
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
.color32(name.as_theme());
(
single_border,
stack_border,
monocle_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
)
}
KomorebiTheme::Base16 {
name,
single_border,
stack_border,
monocle_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
..
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::Base16Value::Base0D)
.color32(*name);
let stack_border = stack_border
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::Base16Value::Base0F)
.color32(*name);
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::Base16Value::Base05)
.color32(*name);
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
(
single_border,
stack_border,
monocle_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
)
}
};
border_manager::FOCUSED.store(u32::from(Colour::from(single_border)), Ordering::SeqCst);
border_manager::MONOCLE
.store(u32::from(Colour::from(monocle_border)), Ordering::SeqCst);
border_manager::STACK.store(u32::from(Colour::from(stack_border)), Ordering::SeqCst);
border_manager::UNFOCUSED
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
STACKBAR_TAB_BACKGROUND_COLOUR.store(
u32::from(Colour::from(stackbar_background)),
Ordering::SeqCst,
);
STACKBAR_FOCUSED_TEXT_COLOUR.store(
u32::from(Colour::from(stackbar_focused_text)),
Ordering::SeqCst,
);
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(
u32::from(Colour::from(stackbar_unfocused_text)),
Ordering::SeqCst,
);
theme_manager::send_notification(*theme);
}
if let Some(path) = &self.app_specific_configuration_path {
let path = resolve_home_path(path)?;
let content = std::fs::read_to_string(path)?;
let asc = ApplicationConfigurationGenerator::load(&content)?;
match path.extension() {
None => {}
Some(ext) => match ext.to_string_lossy().to_string().as_str() {
"yaml" => {
tracing::info!("loading applications.yaml from: {}", path.display());
let path = resolve_home_path(path)?;
let content = std::fs::read_to_string(path)?;
let asc = ApplicationConfigurationGenerator::load(&content)?;
for mut entry in asc {
if let Some(rules) = &mut entry.float_identifiers {
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
}
for mut entry in asc {
if let Some(rules) = &mut entry.ignore_identifiers {
populate_rules(
rules,
&mut ignore_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(ref options) = entry.options {
let options = options.clone();
for o in options {
match o {
ApplicationOptions::ObjectNameChange => {
populate_option(
&mut entry,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
if let Some(ref options) = entry.options {
let options = options.clone();
for o in options {
match o {
ApplicationOptions::ObjectNameChange => {
populate_option(
&mut entry,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::Layered => {
populate_option(
&mut entry,
&mut layered_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::TrayAndMultiWindow => {
populate_option(
&mut entry,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::Force => {
populate_option(
&mut entry,
&mut manage_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::BorderOverflow => {} // deprecated
}
}
}
ApplicationOptions::Layered => {
populate_option(
&mut entry,
&mut layered_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::TrayAndMultiWindow => {
populate_option(
&mut entry,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::Force => {
populate_option(
&mut entry,
&mut manage_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::BorderOverflow => {} // deprecated
}
}
}
"json" => {
tracing::info!("loading applications.json from: {}", path.display());
let path = resolve_home_path(path)?;
let mut asc = ApplicationSpecificConfiguration::load(&path)?;
for entry in asc.values_mut() {
match entry {
AscApplicationRulesOrSchema::Schema(_) => {}
AscApplicationRulesOrSchema::AscApplicationRules(entry) => {
if let Some(rules) = &mut entry.ignore {
populate_rules(
rules,
&mut ignore_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.manage {
populate_rules(
rules,
&mut manage_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.floating {
populate_rules(
rules,
&mut floating_applications,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.transparency_ignore {
populate_rules(
rules,
&mut transparency_blacklist,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.tray_and_multi_window {
populate_rules(
rules,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.layered {
populate_rules(
rules,
&mut layered_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.object_name_change {
populate_rules(
rules,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.slow_application {
populate_rules(
rules,
&mut slow_application_identifiers,
&mut regex_identifiers,
)?;
}
}
}
}
}
_ => {}
},
}
}
@@ -929,7 +1001,34 @@ impl StaticConfig {
pub fn read(path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let value: Self = serde_json::from_str(&content)?;
let mut value: Self = serde_json::from_str(&content)?;
if let Some(path) = &mut value.app_specific_configuration_path {
*path = resolve_home_path(&*path)?;
}
if let Some(monitors) = &mut value.monitors {
for m in monitors {
for w in &mut m.workspaces {
if let Some(path) = &mut w.custom_layout {
*path = resolve_home_path(&*path)?;
}
if let Some(map) = &mut w.custom_layout_rules {
for path in map.values_mut() {
*path = resolve_home_path(&*path)?;
}
}
}
}
}
if let Some(bar_configurations) = &mut value.bar_configurations {
for path in bar_configurations {
*path = resolve_home_path(&*path)?;
}
}
Ok(value)
}
@@ -939,8 +1038,7 @@ impl StaticConfig {
incoming: Receiver<WindowManagerEvent>,
unix_listener: Option<UnixListener>,
) -> Result<WindowManager> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
let mut value = Self::read(path)?;
value.apply_globals()?;
let listener = match unix_listener {
@@ -970,9 +1068,12 @@ impl StaticConfig {
is_paused: false,
virtual_desktop_id: current_virtual_desktop(),
work_area_offset: value.global_work_area_offset,
window_container_behaviour: value
.window_container_behaviour
.unwrap_or(WindowContainerBehaviour::Create),
window_management_behaviour: WindowManagementBehaviour {
current_behaviour: value
.window_container_behaviour
.unwrap_or(WindowContainerBehaviour::Create),
float_override: value.float_override.unwrap_or_default(),
},
cross_monitor_move_behaviour: value
.cross_monitor_move_behaviour
.unwrap_or(MoveBehaviour::Swap),
@@ -987,8 +1088,9 @@ impl StaticConfig {
mouse_follows_focus: value.mouse_follows_focus.unwrap_or(true),
hotwatch: Hotwatch::new()?,
has_pending_raise_op: false,
pending_move_op: None,
pending_move_op: Arc::new(None),
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
uncloack_to_ignore: 0,
};
match value.focus_follows_mouse {
@@ -1019,8 +1121,7 @@ impl StaticConfig {
}
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
let content = std::fs::read_to_string(path)?;
let value: Self = serde_json::from_str(&content)?;
let value = Self::read(path)?;
let mut wm = wm.lock();
if let Some(monitors) = value.monitors {
@@ -1041,12 +1142,9 @@ impl StaticConfig {
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
ws.load_static_config(
monitor
.workspaces
.get(j)
.expect("no static workspace config"),
)?;
if let Some(workspace_config) = monitor.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
}
@@ -1087,8 +1185,7 @@ impl StaticConfig {
}
pub fn reload(path: &PathBuf, wm: &mut WindowManager) -> Result<()> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
let mut value = Self::read(path)?;
value.apply_globals()?;
@@ -1105,12 +1202,9 @@ impl StaticConfig {
);
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
ws.load_static_config(
monitor
.workspaces
.get(j)
.expect("no static workspace config"),
)?;
if let Some(workspace_config) = monitor.workspaces.get(j) {
ws.load_static_config(workspace_config)?;
}
}
}
@@ -1149,7 +1243,11 @@ impl StaticConfig {
}
if let Some(val) = value.window_container_behaviour {
wm.window_container_behaviour = val;
wm.window_management_behaviour.current_behaviour = val;
}
if let Some(val) = value.float_override {
wm.window_management_behaviour.float_override = val;
}
if let Some(val) = value.cross_monitor_move_behaviour {

View File

@@ -0,0 +1,224 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::Colour;
use crate::KomorebiTheme;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use std::ops::Deref;
use std::sync::atomic::Ordering;
use std::sync::OnceLock;
pub struct Notification(KomorebiTheme);
pub static CURRENT_THEME: AtomicCell<Option<KomorebiTheme>> = AtomicCell::new(None);
impl Deref for Notification {
type Target = KomorebiTheme;
fn deref(&self) -> &Self::Target {
&self.0
}
}
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
// Currently this should only be used for async focus updates, such as
// when an animation finishes and we need to focus to set the cursor
// position if the user has mouse follows focus enabled
pub fn send_notification(theme: KomorebiTheme) {
if event_tx().try_send(Notification(theme)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn listen_for_notifications() {
std::thread::spawn(move || loop {
match handle_notifications() {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
}
pub fn handle_notifications() -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
for notification in receiver {
let theme = &notification.0;
let (
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
) = match theme {
KomorebiTheme::Catppuccin {
name,
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
..
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
.color32(name.as_theme());
let stack_border = stack_border
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
.color32(name.as_theme());
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::CatppuccinValue::Pink)
.color32(name.as_theme());
let floating_border = floating_border
.unwrap_or(komorebi_themes::CatppuccinValue::Yellow)
.color32(name.as_theme());
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
.color32(name.as_theme());
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
.color32(name.as_theme());
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::CatppuccinValue::Text)
.color32(name.as_theme());
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
.color32(name.as_theme());
(
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
)
}
KomorebiTheme::Base16 {
name,
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
..
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::Base16Value::Base0D)
.color32(*name);
let stack_border = stack_border
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::Base16Value::Base0F)
.color32(*name);
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
let floating_border = floating_border
.unwrap_or(komorebi_themes::Base16Value::Base09)
.color32(*name);
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::Base16Value::Base05)
.color32(*name);
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
(
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
)
}
};
border_manager::FOCUSED.store(u32::from(Colour::from(single_border)), Ordering::SeqCst);
border_manager::MONOCLE.store(u32::from(Colour::from(monocle_border)), Ordering::SeqCst);
border_manager::STACK.store(u32::from(Colour::from(stack_border)), Ordering::SeqCst);
border_manager::FLOATING.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst);
border_manager::UNFOCUSED
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
STACKBAR_TAB_BACKGROUND_COLOUR.store(
u32::from(Colour::from(stackbar_background)),
Ordering::SeqCst,
);
STACKBAR_FOCUSED_TEXT_COLOUR.store(
u32::from(Colour::from(stackbar_focused_text)),
Ordering::SeqCst,
);
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(
u32::from(Colour::from(stackbar_unfocused_text)),
Ordering::SeqCst,
);
CURRENT_THEME.store(Some(notification.0));
border_manager::send_notification(None);
stackbar_manager::send_notification();
}
Ok(())
}

View File

@@ -1,11 +1,20 @@
use crate::border_manager;
use crate::animation::lerp::Lerp;
use crate::animation::prefix::new_animation_key;
use crate::animation::prefix::AnimationPrefix;
use crate::animation::AnimationEngine;
use crate::animation::RenderDispatcher;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_MANAGER;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::com::SetCloak;
use crate::focus_manager;
use crate::stackbar_manager;
use crate::windows_api;
use crate::ANIMATIONS_IN_PROGRESS;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::AnimationStyle;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
use std::collections::HashMap;
@@ -15,6 +24,7 @@ use std::fmt::Formatter;
use std::fmt::Write as _;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use crate::core::config_generation::IdWithIdentifier;
@@ -35,15 +45,15 @@ use crate::core::ApplicationIdentifier;
use crate::core::HidingBehaviour;
use crate::core::Rect;
use crate::animation::Animation;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::transparency_manager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::FLOAT_IDENTIFIERS;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::NO_TITLEBAR;
@@ -57,16 +67,11 @@ pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
pub struct Window {
pub hwnd: isize,
#[serde(skip)]
animation: Animation,
}
impl From<isize> for Window {
fn from(value: isize) -> Self {
Self {
hwnd: value,
animation: Animation::new(value),
}
Self { hwnd: value }
}
}
@@ -74,7 +79,6 @@ impl From<HWND> for Window {
fn from(value: HWND) -> Self {
Self {
hwnd: value.0 as isize,
animation: Animation::new(value.0 as isize),
}
}
}
@@ -154,11 +158,216 @@ impl Serialize for Window {
}
}
struct MovementRenderDispatcher {
hwnd: isize,
start_rect: Rect,
target_rect: Rect,
top: bool,
style: AnimationStyle,
}
impl MovementRenderDispatcher {
const PREFIX: AnimationPrefix = AnimationPrefix::Movement;
pub fn new(
hwnd: isize,
start_rect: Rect,
target_rect: Rect,
top: bool,
style: AnimationStyle,
) -> Self {
Self {
hwnd,
start_rect,
target_rect,
top,
style,
}
}
}
impl RenderDispatcher for MovementRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> Result<()> {
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
Ok(())
}
fn render(&self, progress: f64) -> Result<()> {
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
WindowsApi::invalidate_rect(self.hwnd, None, false);
Ok(())
}
fn post_render(&self) -> Result<()> {
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top)?;
if ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX)
== 0
{
if WindowsApi::foreground_window().unwrap_or_default() == self.hwnd {
focus_manager::send_notification(self.hwnd)
}
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
Ok(())
}
}
struct TransparencyRenderDispatcher {
hwnd: isize,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
is_opaque: bool,
}
impl TransparencyRenderDispatcher {
const PREFIX: AnimationPrefix = AnimationPrefix::Transparency;
pub fn new(
hwnd: isize,
is_opaque: bool,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
) -> Self {
Self {
hwnd,
start_opacity,
target_opacity,
style,
is_opaque,
}
}
}
impl RenderDispatcher for TransparencyRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(TransparencyRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> Result<()> {
//transparent
if !self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
fn render(&self, progress: f64) -> Result<()> {
WindowsApi::set_transparent(
self.hwnd,
self.start_opacity
.lerp(self.target_opacity, progress, self.style),
)
}
fn post_render(&self) -> Result<()> {
//opaque
if self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
}
impl Window {
pub const fn hwnd(self) -> HWND {
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn move_to_area(&mut self, current_area: &Rect, target_area: &Rect) -> Result<()> {
let current_rect = WindowsApi::window_rect(self.hwnd)?;
let x_diff = target_area.left - current_area.left;
let y_diff = target_area.top - current_area.top;
let x_ratio = f32::abs((target_area.right as f32) / (current_area.right as f32));
let y_ratio = f32::abs((target_area.bottom as f32) / (current_area.bottom as f32));
let window_relative_x = current_rect.left - current_area.left;
let window_relative_y = current_rect.top - current_area.top;
let corrected_relative_x = (window_relative_x as f32 * x_ratio) as i32;
let corrected_relative_y = (window_relative_y as f32 * y_ratio) as i32;
let window_x = current_area.left + corrected_relative_x;
let window_y = current_area.top + corrected_relative_y;
let left = x_diff + window_x;
let top = y_diff + window_y;
let corrected_width = (current_rect.right as f32 * x_ratio) as i32;
let corrected_height = (current_rect.bottom as f32 * y_ratio) as i32;
let new_rect = Rect {
left,
top,
right: corrected_width,
bottom: corrected_height,
};
let is_maximized = &new_rect == target_area;
if is_maximized {
windows_api::WindowsApi::unmaximize_window(self.hwnd);
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled
.get(&MovementRenderDispatcher::PREFIX)
.is_some_and(|v| *v);
drop(animation_enabled);
if move_enabled || ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst) {
let anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
self.set_position(&new_rect, true)?;
let hwnd = self.hwnd;
// Wait for the animation to finish before maximizing the window again, otherwise
// we would be maximizing the window on the current monitor anyway
thread::spawn(move || {
let mut new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
let mut max_wait = 2000; // Max waiting time. No one will be using an animation longer than 2s, right? RIGHT??? WHY?
while new_anim_count > anim_count && max_wait > 0 {
thread::sleep(Duration::from_millis(10));
new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
max_wait -= 1;
}
windows_api::WindowsApi::maximize_window(hwnd);
});
} else {
self.set_position(&new_rect, true)?;
windows_api::WindowsApi::maximize_window(self.hwnd);
}
} else {
self.set_position(&new_rect, true)?;
}
Ok(())
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
let half_width = work_area.right / 2;
let half_weight = work_area.bottom / 2;
@@ -174,53 +383,6 @@ impl Window {
)
}
pub fn animate_position(&self, start_rect: &Rect, target_rect: &Rect, top: bool) -> Result<()> {
let start_rect = *start_rect;
let target_rect = *target_rect;
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
let mut animation = self.animation;
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
border_manager::send_notification();
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
let hwnd = self.hwnd;
std::thread::spawn(move || {
animation.animate(duration, |progress: f64| {
let new_rect = Animation::lerp_rect(&start_rect, &target_rect, progress);
if progress == 1.0 {
WindowsApi::position_window(hwnd, &new_rect, top)?;
if WindowsApi::foreground_window().unwrap_or_default() == hwnd {
focus_manager::send_notification(hwnd)
}
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
.store(false, Ordering::SeqCst);
border_manager::send_notification();
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
} else {
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(hwnd, &new_rect, false)?;
WindowsApi::invalidate_rect(hwnd, None, false);
}
Ok(())
})
});
Ok(())
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
let window_rect = WindowsApi::window_rect(self.hwnd)?;
@@ -228,8 +390,27 @@ impl Window {
return Ok(());
}
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
self.animate_position(&window_rect, layout, top)
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled.get(&MovementRenderDispatcher::PREFIX);
if move_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&MovementRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&MovementRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher =
MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style);
AnimationEngine::animate(render_dispatcher, duration)
} else {
WindowsApi::position_window(self.hwnd, layout, top)
}
@@ -280,7 +461,10 @@ impl Window {
}
pub fn minimize(self) {
WindowsApi::minimize_window(self.hwnd);
let exe = self.exe().unwrap_or_default();
if !exe.contains("komorebi-bar") {
WindowsApi::minimize_window(self.hwnd);
}
}
pub fn close(self) -> Result<()> {
@@ -334,20 +518,81 @@ impl Window {
Ok(())
}
pub fn is_focused(self) -> bool {
WindowsApi::foreground_window().unwrap_or_default() == self.hwnd
}
pub fn transparent(self) -> Result<()> {
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
false,
WindowsApi::get_transparent(self.hwnd).unwrap_or(255),
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
}
}
pub fn opaque(self) -> Result<()> {
let mut ex_style = self.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
true,
WindowsApi::get_transparent(self.hwnd)
.unwrap_or(transparency_manager::TRANSPARENCY_ALPHA.load_consume()),
255,
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)
}
}
pub fn set_accent(self, colour: u32) -> Result<()> {
@@ -534,9 +779,10 @@ pub struct RuleDebug {
pub class: Option<String>,
pub path: Option<String>,
pub matches_permaignore_class: Option<String>,
pub matches_float_identifier: Option<MatchingRule>,
pub matches_ignore_identifier: Option<MatchingRule>,
pub matches_managed_override: Option<MatchingRule>,
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_floating_applications: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
pub matches_no_titlebar: Option<String>,
}
@@ -563,16 +809,16 @@ fn window_is_eligible(
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let float_identifiers = FLOAT_IDENTIFIERS.lock();
let should_float = if let Some(rule) = should_act(
let ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let should_ignore = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&float_identifiers,
&ignore_identifiers,
&regex_identifiers,
) {
debug.matches_float_identifier = Some(rule);
debug.matches_ignore_identifier = Some(rule);
true
} else {
false
@@ -593,7 +839,19 @@ fn window_is_eligible(
false
};
if should_float && !managed_override {
let floating_identifiers = FLOATING_APPLICATIONS.lock();
if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&floating_identifiers,
&regex_identifiers,
) {
debug.matches_floating_applications = Some(rule);
}
if should_ignore && !managed_override {
return false;
}
@@ -735,10 +993,7 @@ pub fn should_act_individual(
let mut should_act = false;
match identifier.matching_strategy {
None => {
panic!("there is no matching strategy identified for this rule");
}
Some(MatchingStrategy::Legacy) => match identifier.kind {
None | Some(MatchingStrategy::Legacy) => match identifier.kind {
ApplicationIdentifier::Title => {
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
should_act = true;

View File

@@ -39,6 +39,7 @@ use crate::core::Rect;
use crate::core::Sizing;
use crate::core::StackbarLabel;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use crate::border_manager;
use crate::border_manager::STYLE;
@@ -71,9 +72,9 @@ use crate::Rgb;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOAT_IDENTIFIERS;
use crate::HIDING_BEHAVIOUR;
use crate::HOME_DIR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
@@ -92,7 +93,7 @@ pub struct WindowManager {
pub is_paused: bool,
pub work_area_offset: Option<Rect>,
pub resize_delta: i32,
pub window_container_behaviour: WindowContainerBehaviour,
pub window_management_behaviour: WindowManagementBehaviour,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub cross_boundary_behaviour: CrossBoundaryBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
@@ -101,8 +102,9 @@ pub struct WindowManager {
pub hotwatch: Hotwatch,
pub virtual_desktop_id: Option<Vec<u8>>,
pub has_pending_raise_op: bool,
pub pending_move_op: Option<(usize, usize, usize)>,
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
pub uncloack_to_ignore: usize,
}
#[allow(clippy::struct_excessive_bools)]
@@ -112,6 +114,7 @@ pub struct State {
pub is_paused: bool,
pub resize_delta: i32,
pub new_window_behaviour: WindowContainerBehaviour,
pub float_override: bool,
pub cross_monitor_move_behaviour: MoveBehaviour,
pub unmanaged_window_operation_behaviour: OperationBehaviour,
pub work_area_offset: Option<Rect>,
@@ -120,6 +123,54 @@ pub struct State {
pub has_pending_raise_op: bool,
}
impl State {
pub fn has_been_modified(&self, wm: &WindowManager) -> bool {
let new = Self::from(wm);
if self.monitors != new.monitors {
return true;
}
if self.is_paused != new.is_paused {
return true;
}
if self.new_window_behaviour != new.new_window_behaviour {
return true;
}
if self.float_override != new.float_override {
return true;
}
if self.cross_monitor_move_behaviour != new.cross_monitor_move_behaviour {
return true;
}
if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour {
return true;
}
if self.work_area_offset != new.work_area_offset {
return true;
}
if self.focus_follows_mouse != new.focus_follows_mouse {
return true;
}
if self.mouse_follows_focus != new.mouse_follows_focus {
return true;
}
if self.has_pending_raise_op != new.has_pending_raise_op {
return true;
}
false
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GlobalState {
@@ -136,7 +187,8 @@ pub struct GlobalState {
pub stackbar_tab_width: i32,
pub stackbar_height: i32,
pub remove_titlebars: bool,
pub float_identifiers: Vec<MatchingRule>,
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Vec<MatchingRule>,
pub manage_identifiers: Vec<MatchingRule>,
pub layered_whitelist: Vec<MatchingRule>,
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
@@ -164,6 +216,9 @@ impl Default for GlobalState {
monocle: Option::from(Colour::Rgb(Rgb::from(
border_manager::MONOCLE.load(Ordering::SeqCst),
))),
floating: Option::from(Colour::Rgb(Rgb::from(
border_manager::FLOATING.load(Ordering::SeqCst),
))),
unfocused: Option::from(Colour::Rgb(Rgb::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
))),
@@ -185,7 +240,7 @@ impl Default for GlobalState {
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
@@ -214,7 +269,8 @@ impl From<&WindowManager> for State {
is_paused: wm.is_paused,
work_area_offset: wm.work_area_offset,
resize_delta: wm.resize_delta,
new_window_behaviour: wm.window_container_behaviour,
new_window_behaviour: wm.window_management_behaviour.current_behaviour,
float_override: wm.window_management_behaviour.float_override,
cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour,
focus_follows_mouse: wm.focus_follows_mouse,
mouse_follows_focus: wm.mouse_follows_focus,
@@ -233,6 +289,7 @@ struct EnforceWorkspaceRuleOp {
origin_workspace_idx: usize,
target_monitor_idx: usize,
target_workspace_idx: usize,
floating: bool,
}
impl EnforceWorkspaceRuleOp {
const fn is_origin(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
@@ -274,7 +331,7 @@ impl WindowManager {
is_paused: false,
virtual_desktop_id: current_virtual_desktop(),
work_area_offset: None,
window_container_behaviour: WindowContainerBehaviour::Create,
window_management_behaviour: WindowManagementBehaviour::default(),
cross_monitor_move_behaviour: MoveBehaviour::Swap,
cross_boundary_behaviour: CrossBoundaryBehaviour::Workspace,
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
@@ -283,8 +340,9 @@ impl WindowManager {
mouse_follows_focus: true,
hotwatch: Hotwatch::new()?,
has_pending_raise_op: false,
pending_move_op: None,
pending_move_op: Arc::new(None),
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
uncloack_to_ignore: 0,
})
}
@@ -307,22 +365,52 @@ impl WindowManager {
StaticConfig::reload(pathbuf, self)
}
pub fn window_container_behaviour(
pub fn window_management_behaviour(
&self,
monitor_idx: usize,
workspace_idx: usize,
) -> WindowContainerBehaviour {
) -> WindowManagementBehaviour {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.workspaces().get(workspace_idx) {
return if workspace.containers().is_empty() {
WindowContainerBehaviour::Create
let current_behaviour =
if let Some(behaviour) = workspace.window_container_behaviour() {
if workspace.containers().is_empty()
&& matches!(behaviour, WindowContainerBehaviour::Append)
{
// You can't append to an empty workspace
WindowContainerBehaviour::Create
} else {
*behaviour
}
} else if workspace.containers().is_empty()
&& matches!(
self.window_management_behaviour.current_behaviour,
WindowContainerBehaviour::Append
)
{
// You can't append to an empty workspace
WindowContainerBehaviour::Create
} else {
self.window_management_behaviour.current_behaviour
};
let float_override = if let Some(float_override) = workspace.float_override() {
*float_override
} else {
self.window_container_behaviour
self.window_management_behaviour.float_override
};
return WindowManagementBehaviour {
current_behaviour,
float_override,
};
}
}
WindowContainerBehaviour::Create
WindowManagementBehaviour {
current_behaviour: WindowContainerBehaviour::Create,
float_override: self.window_management_behaviour.float_override,
}
}
#[tracing::instrument(skip(self))]
@@ -410,6 +498,35 @@ impl WindowManager {
None
}
/// Calculates the direction of a move across monitors given a specific monitor index
pub fn direction_from_monitor_idx(
&self,
target_monitor_idx: usize,
) -> Option<OperationDirection> {
let current_monitor_idx = self.focused_monitor_idx();
if current_monitor_idx == target_monitor_idx {
return None;
}
let current_monitor_size = self.focused_monitor_size().ok()?;
let target_monitor_size = *self.monitors().get(target_monitor_idx)?.size();
if target_monitor_size.left + target_monitor_size.right == current_monitor_size.left {
return Some(OperationDirection::Left);
}
if current_monitor_size.right + current_monitor_size.left == target_monitor_size.left {
return Some(OperationDirection::Right);
}
if target_monitor_size.top + target_monitor_size.bottom == current_monitor_size.top {
return Some(OperationDirection::Up);
}
if current_monitor_size.top + current_monitor_size.bottom == target_monitor_size.top {
return Some(OperationDirection::Down);
}
None
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(self), level = "debug")]
fn add_window_handle_to_move_based_on_workspace_rule(
@@ -420,6 +537,7 @@ impl WindowManager {
origin_workspace_idx: usize,
target_monitor_idx: usize,
target_workspace_idx: usize,
floating: bool,
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
) -> () {
tracing::trace!(
@@ -436,6 +554,7 @@ impl WindowManager {
origin_workspace_idx,
target_monitor_idx,
target_workspace_idx,
floating,
});
}
@@ -491,6 +610,8 @@ impl WindowManager {
};
if matched {
let floating = workspace.floating_windows().contains(window);
if rule.initial_only {
if !already_moved_window_handles.contains(&window.hwnd) {
already_moved_window_handles.insert(window.hwnd);
@@ -502,6 +623,7 @@ impl WindowManager {
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
@@ -513,6 +635,7 @@ impl WindowManager {
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
@@ -531,17 +654,34 @@ impl WindowManager {
// Parse the operation and remove any windows that are not placed according to their rules
for op in &to_move {
let origin_workspace = self
let target_area = *self
.monitors_mut()
.get_mut(op.target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.work_area_size();
let origin_monitor = self
.monitors_mut()
.get_mut(op.origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.ok_or_else(|| anyhow!("there is no monitor with that index"))?;
let origin_area = *origin_monitor.work_area_size();
let origin_workspace = origin_monitor
.workspaces_mut()
.get_mut(op.origin_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
let mut window = Window::from(op.hwnd);
// If it is a floating window move it to the target area
if op.floating {
window.move_to_area(&origin_area, &target_area)?;
}
// Hide the window we are about to remove if it is on the currently focused workspace
if op.is_origin(focused_monitor_idx, focused_workspace_idx) {
Window::from(op.hwnd).hide();
window.hide();
should_update_focused_workspace = true;
}
@@ -571,7 +711,21 @@ impl WindowManager {
.get_mut(op.target_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
target_workspace.new_container_for_window(Window::from(op.hwnd));
if op.floating {
target_workspace
.floating_windows_mut()
.push(Window::from(op.hwnd));
} else {
//TODO(alex-ds13): should this take into account the target workspace
//`window_container_behaviour`?
//In the case above a floating window should always be moved as floating,
//because it was set as so either manually by the user or by a
//`floating_applications` rule so it should stay that way. But a tiled window
//when moving to another workspace by a `workspace_rule` should honor that
//workspace `window_container_behaviour` in my opinion! Maybe this should be done
//on the `new_container_for_window` function instead.
target_workspace.new_container_for_window(Window::from(op.hwnd));
}
}
// Only re-tile the focused workspace if we need to
@@ -696,6 +850,133 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn transfer_window(
&mut self,
origin: (usize, usize, isize),
target: (usize, usize, usize),
) -> Result<()> {
let (origin_monitor_idx, origin_workspace_idx, w_hwnd) = origin;
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
let origin_workspace = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
.workspaces_mut()
.get_mut(origin_workspace_idx)
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
let origin_container_idx = origin_workspace
.container_for_window(w_hwnd)
.and_then(|c| origin_workspace.containers().iter().position(|cc| cc == c));
if let Some(origin_container_idx) = origin_container_idx {
// Moving normal container window
self.transfer_container(
(
origin_monitor_idx,
origin_workspace_idx,
origin_container_idx,
),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
} else if let Some(idx) = origin_workspace
.floating_windows()
.iter()
.position(|w| w.hwnd == w_hwnd)
{
// Moving floating window
// There is no need to physically move the floating window between areas with
// `move_to_area` because the user already did that, so we only need to transfer the
// window to the target `floating_windows`
let floating_window = origin_workspace.floating_windows_mut().remove(idx);
let target_workspace = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no focused workspace for this monitor"))?;
target_workspace
.floating_windows_mut()
.push(floating_window);
} else if origin_workspace
.monocle_container()
.as_ref()
.and_then(|monocle| monocle.focused_window().map(|w| w.hwnd == w_hwnd))
.unwrap_or_default()
{
// Moving monocle container
if let Some(monocle_idx) = origin_workspace.monocle_container_restore_idx() {
let origin_workspace = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.workspaces_mut()
.get_mut(origin_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace for this monitor"))?;
let mut uncloack_amount = 0;
for container in origin_workspace.containers_mut() {
container.restore();
uncloack_amount += 1;
}
origin_workspace.reintegrate_monocle_container()?;
self.transfer_container(
(origin_monitor_idx, origin_workspace_idx, monocle_idx),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
// After we restore the origin workspace, some windows that were cloacked
// by the monocle might now be uncloacked which would trigger a workspace
// reconciliation since the focused monitor would be different from origin.
// That workspace reconciliation would focus the window on the origin monitor.
// So we need to ignore the uncloak events produced by the origin workspace
// restore to avoid that issue.
self.uncloack_to_ignore = uncloack_amount;
}
} else if origin_workspace
.maximized_window()
.as_ref()
.map(|max| max.hwnd == w_hwnd)
.unwrap_or_default()
{
// Moving maximized_window
if let Some(maximized_idx) = origin_workspace.maximized_window_restore_idx() {
self.focus_monitor(origin_monitor_idx)?;
let origin_monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no origin monitor"))?;
origin_monitor.focus_workspace(origin_workspace_idx)?;
self.unmaximize_window()?;
self.focus_monitor(target_monitor_idx)?;
let target_monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no target monitor"))?;
target_monitor.focus_workspace(target_workspace_idx)?;
self.transfer_container(
(origin_monitor_idx, origin_workspace_idx, maximized_idx),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn transfer_container(
&mut self,
@@ -826,7 +1107,7 @@ impl WindowManager {
}
}
} else {
if self.focused_workspace()?.containers().is_empty() {
if self.focused_workspace()?.is_empty() {
let desktop_window = Window::from(WindowsApi::desktop_window()?);
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
@@ -845,6 +1126,8 @@ impl WindowManager {
&& self.focused_workspace()?.maximized_window().is_none()
// and we don't have a monocle container
&& self.focused_workspace()?.monocle_container().is_none()
// and we don't have any floating windows that should show on top
&& self.focused_workspace()?.floating_windows().is_empty()
{
if let Ok(window) = self.focused_window_mut() {
if trigger_focus {
@@ -1129,6 +1412,7 @@ impl WindowManager {
monitor_idx: usize,
workspace_idx: Option<usize>,
follow: bool,
move_direction: Option<OperationDirection>,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -1149,6 +1433,8 @@ impl WindowManager {
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let current_area = *monitor.work_area_size();
let workspace = monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?;
@@ -1157,16 +1443,23 @@ impl WindowManager {
bail!("cannot move native maximized window to another monitor or workspace");
}
let container = workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?;
let container_hwnds = container
.windows()
let foreground_hwnd = WindowsApi::foreground_window()?;
let floating_window_index = workspace
.floating_windows()
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
.position(|w| w.hwnd == foreground_hwnd);
let floating_window =
floating_window_index.map(|idx| workspace.floating_windows_mut().remove(idx));
let container = if floating_window_index.is_none() {
Some(
workspace
.remove_focused_container()
.ok_or_else(|| anyhow!("there is no container"))?,
)
} else {
None
};
monitor.update_focused_workspace(offset)?;
let target_monitor = self
@@ -1174,21 +1467,49 @@ impl WindowManager {
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
target_monitor.add_container(container, workspace_idx)?;
let mut should_load_workspace = false;
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
}
if let Some(workspace) = target_monitor.focused_workspace() {
if !*workspace.tile() {
for hwnd in container_hwnds {
Window::from(hwnd).center(target_monitor.work_area_size())?;
}
if workspace_idx != target_monitor.focused_workspace_idx() {
target_monitor.focus_workspace(workspace_idx)?;
should_load_workspace = true;
}
}
let target_workspace = target_monitor
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no focused workspace on target monitor"))?;
target_monitor.load_focused_workspace(mouse_follows_focus)?;
if let Some(window) = floating_window {
target_workspace.floating_windows_mut().push(window);
Window::from(window.hwnd)
.move_to_area(&current_area, target_monitor.work_area_size())?;
} else if let Some(container) = container {
let container_hwnds = container
.windows()
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
if let Some(direction) = move_direction {
target_monitor.add_container_with_direction(container, workspace_idx, direction)?;
} else {
target_monitor.add_container(container, workspace_idx)?;
}
if let Some(workspace) = target_monitor.focused_workspace() {
if !*workspace.tile() {
for hwnd in container_hwnds {
Window::from(hwnd)
.move_to_area(&current_area, target_monitor.work_area_size())?;
}
}
}
} else {
bail!("failed to find a window to move");
}
if should_load_workspace {
target_monitor.load_focused_workspace(mouse_follows_focus)?;
}
target_monitor.update_focused_workspace(offset)?;
// this second one is for DPI changes when the target is another monitor
@@ -1267,13 +1588,14 @@ impl WindowManager {
tracing::info!("focusing container");
let new_idx = if workspace.monocle_container().is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let new_idx =
if workspace.maximized_window().is_some() || workspace.monocle_container().is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let mut cross_monitor_monocle = false;
let mut cross_monitor_monocle_or_max = false;
// this is for when we are scrolling across workspaces like PaperWM
if new_idx.is_none()
@@ -1350,14 +1672,24 @@ impl WindowManager {
let mouse_follows_focus = self.mouse_follows_focus;
if let Ok(focused_workspace) = self.focused_workspace_mut() {
if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = focused_workspace.maximized_window() {
window.focus(mouse_follows_focus)?;
// (alex-ds13): @LGUG2Z Why was this being done below on the monocle?
// Should it really be done?
//
// WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
// window.hwnd,
// )?)?;
cross_monitor_monocle_or_max = true;
} else if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(mouse_follows_focus)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
window.hwnd,
)?)?;
cross_monitor_monocle = true;
cross_monitor_monocle_or_max = true;
}
} else {
match direction {
@@ -1394,7 +1726,7 @@ impl WindowManager {
}
}
if !cross_monitor_monocle {
if !cross_monitor_monocle_or_max {
if let Ok(focused_window) = self.focused_window_mut() {
focused_window.focus(self.mouse_follows_focus)?;
}
@@ -1473,15 +1805,13 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
{
// remove the container from the origin monitor workspace
let origin_container = self
.focused_workspace_mut()?
.remove_container_by_idx(origin_container_idx)
.ok_or_else(|| {
anyhow!("could not remove container at given origin index")
})?;
self.focused_workspace_mut()?.focus_previous_container();
// actually move the container to target monitor using the direction
self.move_container_to_monitor(
target_monitor_idx,
None,
true,
Some(direction),
)?;
// focus the target monitor
self.focus_monitor(target_monitor_idx)?;
@@ -1501,79 +1831,6 @@ impl WindowManager {
// get a mutable ref to the focused workspace on the target monitor
let target_workspace = self.focused_workspace_mut()?;
match direction {
OperationDirection::Left => {
// insert the origin container into the focused workspace on the target monitor
// at the back (or rightmost position) if we are moving across a boundary to
// the left (back = right side of the target)
match target_workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
target_workspace.add_container_to_front(origin_container);
}
DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace
.insert_container_at_idx(0, origin_container);
} else {
target_workspace
.add_container_to_back(origin_container);
}
}
_ => {
target_workspace.add_container_to_back(origin_container);
}
},
Layout::Custom(_) => {
target_workspace.add_container_to_back(origin_container);
}
}
}
OperationDirection::Right => {
// insert the origin container into the focused workspace on the target monitor
// at the front (or leftmost position) if we are moving across a boundary to the
// right (front = left side of the target)
match target_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(target_workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace
.add_container_to_back(origin_container);
} else {
target_workspace.insert_container_at_idx(
target_index,
origin_container,
);
}
}
_ => {
target_workspace.insert_container_at_idx(
target_index,
origin_container,
);
}
}
}
Layout::Custom(_) => {
target_workspace.add_container_to_front(origin_container);
}
}
}
OperationDirection::Up | OperationDirection::Down => {
// insert the origin container into the focused workspace on the target monitor
// at the position where the currently focused container on that workspace is
target_workspace.insert_container_at_idx(
target_workspace.focused_container_idx(),
origin_container,
);
}
};
// if there is only one container on the target workspace after the insertion
// it means that there won't be one swapped back, so we have to decrement the
// focused position
@@ -1718,7 +1975,12 @@ impl WindowManager {
tracing::info!("cycling container windows");
let container = self.focused_container_mut()?;
let container =
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
container
} else {
self.focused_container_mut()?
};
let len = NonZeroUsize::new(container.windows().len())
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
@@ -1736,13 +1998,51 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn cycle_container_window_index_in_direction(
&mut self,
direction: CycleDirection,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("cycling container window index");
let container =
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
container
} else {
self.focused_container_mut()?
};
let len = NonZeroUsize::new(container.windows().len())
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
if len.get() == 1 {
bail!("there is only one window in this container");
}
let current_idx = container.focused_window_idx();
let next_idx = direction.next_idx(current_idx, len);
container.windows_mut().swap(current_idx, next_idx);
container.focus_window(next_idx);
container.load_focused_window();
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn focus_container_window(&mut self, idx: usize) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("focusing container window at index {idx}");
let container = self.focused_container_mut()?;
let container =
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
container
} else {
self.focused_container_mut()?
};
let len = NonZeroUsize::new(container.windows().len())
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
@@ -1858,8 +2158,16 @@ impl WindowManager {
new_idx
};
let mut target_container_is_stack = false;
if let Some(container) = workspace.containers().get(adjusted_new_index) {
if container.windows().len() > 1 {
target_container_is_stack = true;
}
}
if let Some(current) = workspace.focused_container() {
if current.windows().len() > 1 {
if current.windows().len() > 1 && !target_container_is_stack {
workspace.focus_container(adjusted_new_index);
workspace.move_window_to_container(current_container_idx)?;
} else {

View File

@@ -77,6 +77,7 @@ use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetLayeredWindowAttributes;
use windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
@@ -105,7 +106,6 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
@@ -128,7 +128,6 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
@@ -153,6 +152,7 @@ macro_rules! as_ptr {
};
}
use crate::border_manager::Border;
pub(crate) use as_ptr;
pub enum WindowsResult<T, E> {
@@ -300,14 +300,21 @@ impl WindowsApi {
}
}
if monitors.elements().is_empty() {
monitors.elements_mut().push_back(m);
} else if let Some(preference) = index_preference {
while *preference > monitors.elements().len() {
if let Some(preference) = index_preference {
while *preference >= monitors.elements().len() {
monitors.elements_mut().push_back(Monitor::placeholder());
}
monitors.elements_mut().insert(*preference, m);
let current_name = monitors
.elements_mut()
.get(*preference)
.map_or("", |m| m.name());
if current_name == "PLACEHOLDER" {
let _ = monitors.elements_mut().remove(*preference);
monitors.elements_mut().insert(*preference, m);
} else {
monitors.elements_mut().insert(*preference, m);
}
} else {
monitors.elements_mut().push_back(m);
}
@@ -445,7 +452,13 @@ impl WindowsApi {
}
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
let flags = {
SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_REDRAW
| SetWindowPosition::SHOW_WINDOW
};
Self::set_window_pos(
HWND(as_ptr!(hwnd)),
layout,
@@ -1082,10 +1095,14 @@ impl WindowsApi {
.process()
}
pub fn create_border_window(name: PCWSTR, instance: isize) -> Result<isize> {
pub fn create_border_window(
name: PCWSTR,
instance: isize,
border: *const Border,
) -> Result<isize> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
name,
name,
WS_POPUP | WS_SYSMENU,
@@ -1096,12 +1113,8 @@ impl WindowsApi {
None,
None,
HINSTANCE(as_ptr!(instance)),
None,
)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd
Some(border as _),
)?
}
.process()
}
@@ -1120,6 +1133,21 @@ impl WindowsApi {
Ok(())
}
pub fn get_transparent(hwnd: isize) -> Result<u8> {
unsafe {
let mut alpha: u8 = u8::default();
let mut color_ref = COLORREF(-1i32 as u32);
let mut flags = LWA_ALPHA;
GetLayeredWindowAttributes(
HWND(as_ptr!(hwnd)),
Some(std::ptr::addr_of_mut!(color_ref)),
Some(std::ptr::addr_of_mut!(alpha)),
Some(std::ptr::addr_of_mut!(flags)),
)?;
Ok(alpha)
}
}
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> Result<isize> {
unsafe {
CreateWindowExW(

View File

@@ -1,10 +1,6 @@
use std::collections::VecDeque;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use crate::border_manager;
use crate::container::Container;
use crate::window::RuleDebug;
use crate::window::Window;
@@ -12,6 +8,19 @@ 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;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongW;
use windows::Win32::UI::WindowsAndMessaging::SendNotifyMessageW;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
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;
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
@@ -60,6 +69,15 @@ pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
fn has_filtered_style(hwnd: HWND) -> bool {
let style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) as u32 };
let ex_style = unsafe { GetWindowLongW(hwnd, GWL_EXSTYLE) as u32 };
style & WS_CHILD.0 != 0
|| ex_style & WS_EX_TOOLWINDOW.0 != 0
|| ex_style & WS_EX_NOACTIVATE.0 != 0
}
pub extern "system" fn win_event_hook(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
@@ -69,8 +87,7 @@ pub extern "system" fn win_event_hook(
_id_event_thread: u32,
_dwms_event_time: u32,
) {
// OBJID_WINDOW
if id_object != 0 {
if id_object != OBJID_WINDOW.0 {
return;
}
@@ -81,6 +98,23 @@ pub extern "system" fn win_event_hook(
Err(_) => return,
};
// this forwards the message to the window's border when it moves or is destroyed
// see border_manager/border.rs
if matches!(
winevent,
WinEvent::ObjectLocationChange | WinEvent::ObjectDestroy
) && !has_filtered_style(hwnd)
{
let border_window = border_manager::window_border(hwnd.0 as isize);
if let Some(border) = border_window {
unsafe {
let _ =
SendNotifyMessageW(border.hwnd(), event, WPARAM(0), LPARAM(hwnd.0 as isize));
}
}
}
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => {
tracing::trace!(

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