Compare commits

..

66 Commits

Author SHA1 Message Date
LGUG2Z
c12afe5521 chore(deps): cargo update 2025-01-08 21:37:17 -08:00
LGUG2Z
2d97ee101d feat(config): use global padding when omitted on ws
Simplifying my software for the masses
2025-01-08 20:47:16 -08:00
LGUG2Z
a4f69238b7 fix(wm): preserve new padding when loading state
This commit is a follow up to 7bf1521363,
ensuring that if a user has changed global padding options, that they
will be preserved from the initialized window manager state and applied
on top of the dumped state which is being restored.
2025-01-08 20:24:23 -08:00
alex-ds13
96f7eb1d31 fix(bar): apply position on start
For some reason, when calling the `window.set_position` when creating
the Komobar or even when applying the config on the first frame the
actual EGUI's window size wasn't changing. This commit adds a new field
to `Komobar` called `size_rect` so that we can store the expected size
rect of the window according to the config, so that we don't have to be
calculating it all the time. This field is updated on `apply_config`.

Now on `update` of the bar we check if the current size using the EGUI
Context is the expected `size_rect`, if it is we do nothing, if it is
not we update the bar position. This makes sure that on start the bar
will resize to the users config correctly! Now the resize of the bar
only happens here.

This commit also adds the `hwnd` field to `Komobar` so that we don't
have to be calling `process_hwnd()` all the time.
2025-01-07 16:39:38 -08:00
LGUG2Z
28cd4a8801 fix(wm): skip destroyed windows on rule enforcement
This commit introduces an if let binding to only process windows which
still exist when attempting to enforce workspace rules.

Previously, calls to functions such as Window::exe might have returned
an error if a window which had been destroyed but not yet removed from
the state was examined by the enforce_workspace_rules fn. Now, such
windows will fail the if let binding and be skipped entirely, eventually
being removed by the core event processing loop.
2025-01-04 21:14:33 -08:00
LGUG2Z
3aa92a1255 feat(bar): add update widget
This commit adds a new widget, "Update", which will check for komorebi
version updates using the cargo package version of the running binary
and the latest release returned from the GitHub API.

If the latest release is newer than the current cargo package version, a
widget will be shown, which can be clicked to open the changelog of the
latest release.
2025-01-04 21:14:33 -08:00
LGUG2Z
281980b010 fix(wm): avoid obvious border manager thread crash
This commit adds an early exit from the border manager's event
processing loop whenever a window which still exists in the state but
has been destroyed is encountered. Instead of returning an error, the
'containers loop will now skip ahead to the next iteration.

This commit also makes an adjustment to the frequency with which the
reaper sends border manager notifications - a single notification is now
sent at the end of each iteration if necessary, rather than one
notification per workspace.
2025-01-04 21:14:33 -08:00
LGUG2Z
c063302c91 feat(cli): add stackbar-mode command
This commit adds a new komorebic command "stackbar-mode" to allow users
to change stackbar modes programmatically.
2025-01-04 21:14:33 -08:00
LGUG2Z
ba52dc3378 fix(wm): add uncloak as a notif override event
If a user triggers the workspace reconciliator by clicking on an app in
the start bar or via alt-tab, a notification should be sent to
subscribers such as komorebi-bar so that the focused workspace can be
updated.

The various komorebi reconciliators and manager modules don't emit
events to subscribers themselves (yet?), so for now we can pass on the
uncloak event.

Maybe we can look into expanding the Notification enum in the future.

fix #1211
2025-01-04 21:14:33 -08:00
LGUG2Z
44716fdc98 fix(wm): avoid focused ws rule enforcement deadlock
This commit adds mutex lock scoping in
WindowManager::enforce_workspace_rule to avoid a deadlock when
should_update_focused_workspace evaluates to true.

fix #1212
2025-01-04 21:14:33 -08:00
LGUG2Z
4b30cecba9 feat(config): allow specifying layout flip on ws
This commit adds support for specifying a layout flip axis for each
workspace in the static configuration file.
2025-01-04 21:14:33 -08:00
LGUG2Z
d45cd729e8 feat(cli): allow checking of arbitrary config files
This commit adds an optional --komorebi-config flag to the check command
to allow users to check a komorebi.json file in an arbitrary location.
2025-01-04 21:14:27 -08:00
LGUG2Z
5a8f48c6b9 chore(dev): begin v0.1.33-dev 2025-01-03 18:20:23 -08:00
LGUG2Z
4b9d811499 chore(release): v0.1.32 2025-01-01 11:23:43 -08:00
LGUG2Z
d520a2bf74 docs(mkdocs): run docgen 2025-01-01 10:43:19 -08:00
LGUG2Z
7ef4fd81c0 feat(cli): add version update checks
This commit adds version update checks and feedback to the komorebic
start and check commands.
2024-12-31 10:58:25 -08:00
LGUG2Z
083ab65077 feat(docs): individual commercial use licensing
This commit updates various docs with information on the long-promised
individual commercial use license which will be available to purchase
from 01 Jan 2025 onwards.
2024-12-31 10:02:40 -08:00
LGUG2Z
e9bb6b43d6 feat(cli): add eager-focus command
This commit adds a new komorebic command "eager-focus", which takes a
full case-sensitive exe identifier as an argument. When komorebi
receives this message, it will look through each monitor and workspace
for the first matching managed window and then focus it.

This allows users who have well defined workspaces and rules to bind
semantic hotkeys to commands like "komorebic eager-focus Discord.exe" to
immediately jump to applications instead of mentally looking up their
assigned workspaces or positions within container stacks.
2024-12-29 12:29:23 -08:00
LGUG2Z
79eda30f48 feat(config): add matchers for removing titlebars
This commit adds a new field to the static config file, "remove_titlebar_applications", which allows
users to now use the full range of matching strategies to identify applications for which titlebars
should be removed. This is heavily discouraged for a number of reasons, and is unlikely to work with
a wide range of applications which now draw their own titlebar regions. The previous advice to use
in-application configuration settings to hide title bars if they exist is still valid.

resolve #805
2024-12-27 11:37:00 -08:00
alex-ds13
692da90890 feat(wm): allow reapplying initial workspace rules
This commit adds the following new socket messages and commands:
- `EnforceWorkspaceRules`: resets the `already_moved_window_handles` and
  calls `enforce_workspace_rules` so that all workspace rules, including
  initial workspace rules are applied again
- `enforce-workspace-rules`: cli command which sends the
  EnforceWorkspaceRules socket message
2024-12-26 14:16:25 -08:00
alex-ds13
4babf336ec fix(bar): prevent komorebi connection from staling
Sometimes the bar would randomly stop receiving notifications from
komorebi and would stop updating the `Komorebi` widget.

This feels to me that the reason is the same one that used to happen on
the `process_commands` from `komorebi` where the socket would get stuck
reading an empty connection.

This commit adds a read timeout to the socket to prevent that from
happening and hopefully it should stop those situations where the bar
would stop receiving notifications.
2024-12-26 14:16:13 -08:00
LGUG2Z
53a83eedb5 chore(deps): cargo update 2024-12-26 13:59:38 -08:00
dependabot[bot]
e1bbd3c1f5 chore(deps): bump houseabsolute/actions-rust-cross from 0 to 1
Bumps [houseabsolute/actions-rust-cross](https://github.com/houseabsolute/actions-rust-cross) from 0 to 1.
- [Release notes](https://github.com/houseabsolute/actions-rust-cross/releases)
- [Changelog](https://github.com/houseabsolute/actions-rust-cross/blob/v0/Changes.md)
- [Commits](https://github.com/houseabsolute/actions-rust-cross/compare/v0...v1)

---
updated-dependencies:
- dependency-name: houseabsolute/actions-rust-cross
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-26 13:59:38 -08:00
LGUG2Z
2c08fbe8f6 fix(wm): focus prev idx when closing workspace 2024-12-23 16:56:22 -08:00
Csaba
cced2a4433 feat(bar): added icon_scale to the config allowing a custom value between 1.0 and 2.0 2024-12-22 15:00:11 -08:00
alex-ds13
d93b6fa1b3 fix(bar): update widgets background color properly
Previously when changing between themes with different backgrounds the
widget's background color was not updating because they take the bg
color from the `RenderConfig` which was only being updated on
`apply_config`, now we also pass the `RenderConfig` to the `apply_theme`
function and update it's `background_color` there as well.
2024-12-21 08:53:01 -08:00
Csaba
99353b8064 fix(bar): network widget spacing 2024-12-19 21:01:11 -08:00
LGUG2Z
c64a42bca5 chore(deps): bump egui to v0.30 2024-12-19 17:00:41 -08:00
alex-ds13
5ab5ec4f3a fix(bar): apply theme on first frame
On some computers the context colors were being reset on the very first
frame. So now we try to apply the theme on the first frame and
afterwards we only do it again when there is a config change or a theme
socket message.
2024-12-19 16:39:35 -08:00
alex-ds13
ad08585faf fix(bar): use layout on Area to prevent shaking
There were some cases were the bar was showing some shaking, turns out
that using `ui.with_layout` instead of `ui.horizontal_centered` removes
this shaking, so this commit makes that change and uses the
`right_to_left` layout on the right widgets again, meaning that we need
to reverse them again.
2024-12-19 16:39:35 -08:00
alex-ds13
eb8a988841 fix(bar): fix background color clobbering 2024-12-19 16:39:35 -08:00
alex-ds13
0e2a55b300 fix(bar): apply roundings on komorebi.json change
Group roundings were getting lost when applying the theme after a
`komorebi.json` change/save trigger. Now we reapply these groupings on
the `apply_theme` to make sure they are always correct.
2024-12-19 16:39:35 -08:00
alex-ds13
eda91dcd1d fix(bar): use bg color before applying transparency 2024-12-19 16:39:35 -08:00
alex-ds13
0c6317a27b fix(bar): use correct transparency_alpha
Previously when reading the `theme` from `komorebi.json` it was also
getting the transparency_alpha from the `StaticConfig`, this is wrong,
it should use the alpha from the bar config. This commit fixes that.
2024-12-19 16:39:35 -08:00
alex-ds13
5c81a8c9e2 fix(bar): apply work_area_offset on config change 2024-12-19 16:39:35 -08:00
alex-ds13
a4128b7276 fix(bar): handle komorebi theme change properly
Previously if we changed/set the theme on `komorebi.json` it would apply
that theme to the bar without taking into account the transparency
alpha, also after removing the `theme` from `komorebi.json` file it
wasn't applying the theme from the bar config. This commit fixes these
issues.
2024-12-19 16:39:35 -08:00
alex-ds13
73a4df884c fix(bar): use the frame.inner_margin config 2024-12-19 16:39:35 -08:00
alex-ds13
32a234317c fix(bar): actually save the config on apply_config 2024-12-19 16:39:35 -08:00
alex-ds13
0dc6780da6 fix(bar): normalize areas of widgets
This commit changes the way each of the 3 parts of potential widgets
(left, center and right) is created so that they are all done on the
same way and look the same. It is using `Area` with different anchors
for each part which makes the widgets actually center vertically
properly.

This created an issue with the `Bar` grouping. To fix it we've made the
`Bar` grouping change the outer panel frame instead of creating an
actual group. This has the side effect (or maybe feature!) of losing the
background of the outer frame. Meaning this outer frame will now have
the look of the `Bar` grouping only. Currently it is using a fixed outer
margin but this can be changed in the future to a config option.
2024-12-19 16:39:35 -08:00
alex-ds13
f089d3e59b feat(wm): allow stopping without restoring windows
This commit creates a new `SocketMessage` called `StopIgnoreRestore`
which makes komorebi stop without calling `window.restore()` on all
windows. This way every maximized window will stay maximized once you
start komorebi again and it is able to use the previous `State`.

If it fails to restore the previous state you might have to call
`komorebic restore-windows` in case you had hidden windows, for example
when when using the `window_hiding_behaviour` as `Hide`, or you can
simply unminimize them if you were using `Cloak` or `Minimize`.
2024-12-18 08:19:21 -08:00
LGUG2Z
5dbf0f1b89 docs(schema): update all json schemas 2024-12-17 19:38:30 -08:00
alex-ds13
d393f8fe77 feat(bar): add two new display format types
This commit adds two new `DisplayFormat` types:
- `TextAndIconOnSelected`: which displays icon and text for the selected
  element and the other elements only have text.
- `IconAndTextOnSelected`: which displays icon and text for the selected
  element and the other elements only have icon.
2024-12-17 16:00:08 -08:00
alex-ds13
c3769e7881 feat(bar): optional workspaces on Komorebi widget
This commit makes the `workspaces` on `Komorebi` widget optional. This
way it allows adding the `workspaces` on one Alignment and the
`focused_window` on another one, for example.
2024-12-17 14:43:59 -08:00
Csaba
3c0b12f9af feat(bar): scale icon size with font size
This commit changes the way icons are displayed on the bar.

There was an issue with how app icons were sized using shrink_to_fit.

This has been changed to use fit_to_exact_size instead, relying on the
font size as a starting point and scaling it to 1.4 of its size, making
the icons to appear larger.

The same scaling was done to all the widget icons as well to make them
look unified.
2024-12-17 12:58:36 -08:00
alex-ds13
804faef229 fix(wm): focus and update after apply state
This commit makes sure we focus the previously focused workspace on all
monitors, load it and update it and in the end focus the actual focused
monitor and workspace pair calling `update_focused_workspace` to make
sure it updates the workspace and gives focus to the focused window.
2024-12-17 11:51:55 -08:00
LGUG2Z
7bf1521363 feat(wm): dump and load previous instance state
This commit adds changes to the main wm process to dump a state file to
temp_dir() when the process is exited either via komorebic stop or
ctrl-c, and to automatically try to reload that dumped state file if it
exists on the next run.

A new flag "--clean-state" has been added to both komorebi.exe and the
komorebic start command to override this behaviour.

The dumped state file can only be applied if the number of connected
monitors matches the number of monitors recorded in the state, and if
every HWND listed in the state file still exists.

This is validated by calling Window.exe(), which under the hood checks
for the continued existence of the process associated with the HWND.

Only the "workspace" subsection of the state for each matching
connecting monitor will be applied.
2024-12-17 08:33:02 -08:00
LGUG2Z
b49e634b65 feat(wm): add transparency config to global state
This commit adds various transparency related global configuration
values to GlobalState, which is can be queried via the komorebic
global-state command.

resolve #1182
2024-12-16 18:54:29 -08:00
LGUG2Z
be0671be6d fix(cli): correct copy-paste typo in autostart
This commit corrects a typo which adds the "--masir" flag to the
autostart shortcut when the user has passed the "--bar" flag to the
enable-autostart command.

fix #1178
2024-12-14 22:52:35 -08:00
alex-ds13
10539a4bab fix(bar): prevent the bar from changing mff value
This commit makes use of the new `send_batch` function to batch all the
messages in one go when pressing the button to move between workspaces
or when moving between stacked windows.

Since we are creating this messages in one go we won't be mistakenly
changing the value of mff for the user.

It also only batches the mff messages when the mff value it's true, if
it is already false there is no need to be sending those extra messages.
2024-12-14 08:59:46 -08:00
alex-ds13
9463c75f12 feat(client): create send_batch helper
This commit adds a helper function `send_batch` to komorebi-client that
allows sending multiple messages in a batch.

3rd party users of this library could already do this themselves but it
is nice to have this helper to simplify it.
2024-12-14 08:59:29 -08:00
LGUG2Z
c31c5dc69d chore(just): split schemagen into windows and nixos jobs 2024-12-14 08:57:45 -08:00
LGUG2Z
6c07863b81 chore(dev): begin v0.1.32-dev 2024-12-14 08:53:42 -08:00
LGUG2Z
40c55dec39 chore(release): v0.1.31 2024-12-13 16:48:35 -08:00
LGUG2Z
5cc2d9d469 chore(deps): cargo update 2024-12-13 16:05:20 -08:00
LGUG2Z
91b255280a ci(github): bump winget-releaser from v2 to main 2024-12-13 16:04:10 -08:00
LGUG2Z
9bd1073a83 perf(bar): add icon cache
This commit adds an icon cache which is indexed by executable name to
avoid unnecessary calls to windows_icons::get_icon_by_process_id, which
is known to start failing after the komorebi-bar process has been
running for a certain (unknown) period of time.
2024-12-10 16:23:37 -08:00
LGUG2Z
53c1990442 docs(schema): update all json schemas 2024-12-09 17:04:18 -08:00
Csaba
9d6173ecbb feat(bar): only collect enabled widgets 2024-12-09 15:11:25 -08:00
Csaba
830da89529 feat(bar): network widget - added show_default_interface and use enable to toggle the whole widget 2024-12-09 15:11:25 -08:00
Csaba
f59d7a51f1 feat(bar): indicate clickable widgets 2024-12-09 15:11:25 -08:00
Csaba
1470c63cfe feat(bar): 5 new grouping styles for shadow and glow 2024-12-09 15:11:25 -08:00
Csaba
64382b18c1 fix(bar): only indicate focused window on stack 2024-12-09 15:11:25 -08:00
alex-ds13
26f90cc9ee fix(borders): floating window z-order handling
This commit makes it so a floating window only has the floating border
when it is focused, if not it has the `Unfocused` border. It also makes
the 'focused_container' have the `Unfocused` border when it is not the
foreground window, for example when we have a floating window focused
instead.

This commit also changes the border's `window_kind` so that the stored
borders actually have that value so we can check it later (This value
wasn't being updated).

This commit also makes it so we properly invalidate the borders in the
situations discussed above (for example when changing focus to/from a
floating window we need the floating window border to update its ZOrder
as well as the previously focused window).

Lastly this commit, changes the `WM_PAINT` code part of the border so
that it now sets the position of border so that the border's ZOrder
updates to it's tracking window ZOrder.
2024-12-09 14:53:14 -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
69 changed files with 4092 additions and 1361 deletions

View File

@@ -1,6 +1,6 @@
name: Bug report
description: File a bug report
labels: [ bug ]
labels: [bug]
title: "[BUG]: "
body:
- type: markdown

View File

@@ -47,7 +47,7 @@ jobs:
key: ${{ matrix.platform.target }}
- run: cargo +nightly fmt --check
- run: cargo clippy
- uses: houseabsolute/actions-rust-cross@v0
- uses: houseabsolute/actions-rust-cross@v1
with:
command: "build"
target: ${{ matrix.platform.target }}
@@ -199,7 +199,7 @@ jobs:
needs: release
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
- uses: vedantmgoyal2009/winget-releaser@main
with:
identifier: LGUG2Z.komorebi
token: ${{ secrets.WINGET_TOKEN }}

717
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.29"
egui_extras = "0.29"
eframe = "0.30"
egui_extras = "0.30"
dirs = "5"
dunce = "1"
hotwatch = "0.5"
@@ -31,13 +31,13 @@ tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.31"
sysinfo = "0.33"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.35"
shadow-rs = "0.37"
which = "7"
[workspace.dependencies.windows]

115
README.md
View File

@@ -29,6 +29,8 @@ Tiling Window Management for Windows.
![screenshot](https://user-images.githubusercontent.com/13164844/184027064-f5a6cec2-2865-4d65-a549-a1f1da589abf.png)
## Overview
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
[Desktop Window
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
@@ -50,6 +52,8 @@ _komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflow
[configuration schema reference](https://komorebi.lgug2z.com/schema) and a
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
## Community
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
_komorebi_-related discussion, help, troubleshooting etc. If you have any
specific feature requests or bugs to report, please create an issue in this
@@ -57,28 +61,62 @@ repository.
There is a [YouTube
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.
_komorebi_ development videos, feature previews and release overviews. Subscribing
to the channel (which is monetized as part of the YouTube Partner Program) and
watching videos is a really simple and passive way to contribute financially to
the development and maintenance of _komorebi_.
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
## Licensing for Personal Use
`komorebi` is licensed under the [Komorebi 1.0.0
license](https://github.com/LGUG2Z/komorebi-license), 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` for
personal use other than redistribution, or distribution of new works (i.e.
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](https://github.com/LGUG2Z/komorebi-license) does
not permit any kind of commercial use (i.e. using `komorebi` at work).
## Sponsorship for Personal Use
_komorebi_ is a free and educational source 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) or contributing
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
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
anything specific to offer besides my gratitude and shout outs at the end of
_komorebi_ live development videos and tutorials.
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
the Discord server, get shout outs at the end of _komorebi_-related videos on
YouTube, and gain the ability to submit feature requests on the issue tracker.
If you would like to tip or sponsor the project but are unable to use GitHub
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z).
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
## Licensing for Commercial Use
A dedicated Individual Commercial Use License is available for those who want to
use `komorebi` at work.
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
for the licensed individual only, for the duration of a valid paid license
subscription only. All provisions and restrictions enumerated in the [Komorebi
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
More information, pricing and purchase links for Individual Commercial Use
Licenses [can be found here](https://lgug2z.com/software/komorebi).
# Installation
@@ -125,7 +163,11 @@ https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-
# Contribution Guidelines
If you would like to contribute to `komorebi` please take the time to carefully read the guidelines below.
If you would like to contribute to `komorebi` please take the time to carefully
read the guidelines below.
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
code contributions to `komorebi` are licensed.
## Commit hygiene
@@ -135,8 +177,8 @@ If you would like to contribute to `komorebi` please take the time to carefully
- Use `git cz` with
the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare
commit messages
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the
changes being committed
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for
the changes being committed
## PRs should contain only a single feature or bug fix
@@ -175,7 +217,8 @@ This includes but is not limited to:
- All `komorebic` commands
- The `komorebi.json` schema
- The [`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
- The [
`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
schema
No user should ever find that their configuration file has stopped working after upgrading to a new version
@@ -191,27 +234,6 @@ ability for users to specify colours in `komorebi.json` in Hex format alongside
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
required.
## License
`komorebi` is licensed under the [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` 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.
# Development
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
@@ -220,13 +242,13 @@ the IDE for completions and navigation:
- Set `Expand declarative macros`
to `Use new engine` under "Settings > Langauges & Frameworks > Rust"
- Enable the following experimental features:
- `org.rust.cargo.evaluate.build.scripts`
- `org.rust.macros.proc`
- `org.rust.cargo.evaluate.build.scripts`
- `org.rust.macros.proc`
# Logs and Debugging
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or overwritten, so it will keep
growing until it is deleted by the user.
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or
overwritten, so it will keep growing until it is deleted by the user.
Whenever running the `komorebic stop` command or sending a Ctrl-C signal to `komorebi` directly, the `komorebi` process
ensures that all hidden windows are restored before termination.
@@ -367,7 +389,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.30"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.32"}
use anyhow::Result;
use komorebi_client::Notification;
@@ -442,12 +464,17 @@ programming languages.
# Appreciations
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its never-ending development
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its
never-ending development
- Thank you to [@sitiom](https://github.com/sitiom) for being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
- Thank you to [@sitiom](https://github.com/sitiom) for
being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught me more than I can ever hope to repay
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught
me more than I can ever hope to repay
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling window management on Windows with me and having an excellent spirit of collaboration
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling
window management on Windows with me and having an excellent spirit of collaboration
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring
the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`

View File

@@ -3,13 +3,18 @@
```
Set the duration for movement animations in ms
Usage: komorebic.exe animation-duration <DURATION>
Usage: komorebic.exe animation-duration [OPTIONS] <DURATION>
Arguments:
<DURATION>
Desired animation durations in ms
Options:
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the duration to. If not specified, sets global duration
[possible values: movement, transparency]
-h, --help
Print help

View File

@@ -10,8 +10,14 @@ 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]
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the style to. If not specified, sets global style
[possible values: movement, transparency]
-h, --help
Print help

View File

@@ -3,13 +3,18 @@
```
Enable or disable movement animations
Usage: komorebic.exe animation <BOOLEAN_STATE>
Usage: komorebic.exe animation [OPTIONS] <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the state to. If not specified, sets global state
[possible values: movement, transparency]
-h, --help
Print help

View File

@@ -3,9 +3,12 @@
```
Check komorebi configuration and related files for common errors
Usage: komorebic.exe check
Usage: komorebic.exe check [OPTIONS]
Options:
-k, --komorebi-config <KOMOREBI_CONFIG>
Path to a static configuration JSON file
-h, --help
Print help

View File

@@ -0,0 +1,12 @@
# close-workspace
```
Close the focused workspace (must be empty and unnamed)
Usage: komorebic.exe close-workspace
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,16 @@
# cycle-stack-index
```
Cycle the index of the focused window in the focused stack in the specified cycle direction
Usage: komorebic.exe cycle-stack-index <CYCLE_DIRECTION>
Arguments:
<CYCLE_DIRECTION>
[possible values: previous, next]
Options:
-h, --help
Print help
```

16
docs/cli/eager-focus.md Normal file
View File

@@ -0,0 +1,16 @@
# eager-focus
```
Focus the first managed window matching the given exe
Usage: komorebic.exe eager-focus <EXE>
Arguments:
<EXE>
Case-sensitive exe identifier
Options:
-h, --help
Print help
```

View File

@@ -18,6 +18,9 @@ Options:
--bar
Enable autostart of komorebi-bar
--masir
Enable autostart of masir
-h, --help
Print help

View File

@@ -0,0 +1,12 @@
# enforce-workspace-rules
```
Enforce all workspace rules, including initial workspace rules that have already been applied
Usage: komorebic.exe enforce-workspace-rules
Options:
-h, --help
Print help
```

24
docs/cli/kill.md Normal file
View File

@@ -0,0 +1,24 @@
# kill
```
Kill background processes started by komorebic
Usage: komorebic.exe kill [OPTIONS]
Options:
--whkd
Kill whkd if it is running as a background process
--ahk
Kill ahk if it is running as a background process
--bar
Kill komorebi-bar if it is running as a background process
--masir
Kill masir if it is running as a background process
-h, --help
Print help
```

18
docs/cli/stackbar-mode.md Normal file
View File

@@ -0,0 +1,18 @@
# stackbar-mode
```
Set the stackbar mode
Usage: komorebic.exe stackbar-mode <MODE>
Arguments:
<MODE>
Desired stackbar mode
[possible values: always, never, on-stack]
Options:
-h, --help
Print help
```

View File

@@ -24,6 +24,12 @@ Options:
--bar
Start komorebi-bar in a background process
--masir
Start masir in a background process for focus-follows-mouse
--clean-state
Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
-h, --help
Print help

View File

@@ -15,6 +15,9 @@ Options:
--bar
Stop komorebi-bar if it is running as a background process
--masir
Stop masir if it is running as a background process
-h, --help
Print help

View File

@@ -1,7 +1,8 @@
# 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
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

View File

@@ -181,10 +181,10 @@ The `grid` layout does not support resizing windows tiles.
key bindings go to the left of the colon, and shell commands go to the right of the
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,
I recommend using [AutoHotKey](https://autohotkey.com) to set up your key
bindings for `komorebic` commands instead.
As of [`v0.2.4`](https://github.com/LGUG2Z/whkd/releases/tag/v0.2.4), `whkd` can override most of Microsoft's
limitations on hotkey bindings that include the `win` key. However, you will still need
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
`win + l` from locking the operating system.
```
{% include "./whkdrc.sample" %}
@@ -203,7 +203,7 @@ It is also possible to change a hotkey behavior depending on which application h
alt + n [
# ProcessName as shown by `Get-Process`
Firefox : echo "hello firefox"
# Spaces are fine, no quotes required
Google Chrome : echo "hello chrome"
]

View File

@@ -1,5 +1,7 @@
![screenshot](https://user-images.githubusercontent.com/13164844/184027064-f5a6cec2-2865-4d65-a549-a1f1da589abf.png)
## Overview
`komorebi` is a tiling window manager that works as an extension to Microsoft's
[Desktop Window
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
@@ -15,12 +17,63 @@ system and desktop environment by default. Users are free to make such
modifications in their own configuration files for `komorebi`, but these will
always remain opt-in and off-by-default.
## Community
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
`komorebi`-related discussion, help, troubleshooting etc. If you have any
specific feature requests or bugs to report, please create an issue on
[GitHub](https://github.com/LGUG2Z/komorebi).
`komorebi`-related discussion, help, troubleshooting etc.
There is a [YouTube
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
`komorebi` development videos, feature previews and release overviews. Subscribing
to the channel (which is monetized as part of the YouTube Partner Program) and
watching videos is a really simple and passive way to contribute financially to
the development and maintenance of `komorebi`.
There is also a [YouTube
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg?sub_confirmation=1)
where I share `komorebi` live programming videos and tutorial videos.
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
showcases the many awesome projects that exist in the `komorebi` ecosystem.
## Licensing for Personal Use
`komorebi` is licensed under the [Komorebi 1.0.0 license](https://github.com/LGUG2Z/komorebi-license), 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` for personal use other than redistribution, or distribution of
new works (i.e. 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](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
i.e. using `komorebi` at work).
## Sponsorship for Personal Use
`komorebi` is a free and educational source 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) or to contribute
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). Sponsors can claim custom roles on
the Discord server, get shout-outs at the end of _komorebi_-related videos on
YouTube, and gain the ability to submit feature requests on the issue tracker.
If you would like to tip or sponsor the project but are unable to use GitHub
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
## Licensing for Commercial Use
A dedicated Individual Commercial Use License is available for those who want to
use `komorebi` at work.
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
for the licensed individual only, for the duration of a valid paid license
subscription only. All provisions and restrictions enumerated in the [Komorebi
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
More information, pricing and purchase links for Individual Commercial Use
Licenses [can be found here](https://lgug2z.com/software/komorebi).

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.32/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {
@@ -33,6 +33,11 @@
}
],
"right_widgets": [
{
"Update": {
"enable": true
}
},
{
"Media": {
"enable": true
@@ -73,4 +78,4 @@
}
}
]
}
}

View File

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

View File

@@ -1,4 +1,5 @@
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
export RUST_BACKTRACE := "full"
clean:
@@ -45,13 +46,15 @@ docgen:
cargo run --package komorebic -- docgen
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
schemagen:
jsonschema:
cargo run --package komorebic -- static-config-schema > schema.json
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
cargo run --package komorebi-bar -- --schema > schema.bar.json
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
rm -Force .\bar-config-docs\schema.html
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html
# this part is run in a nix shell because python is a nightmare
schemagen:
rm -rf static-config-docs bar-config-docs
mkdir -p static-config-docs bar-config-docs
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.31"
version = "0.1.33"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -16,15 +16,16 @@ crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = "0.7"
egui-phosphor = "0.8"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.31"
netdev = "0.32"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

View File

@@ -38,6 +38,7 @@ use eframe::egui::Visuals;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
use komorebi_client::SocketMessage;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Catppuccin;
@@ -49,6 +50,7 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct Komobar {
pub hwnd: Option<isize>,
pub config: Arc<KomobarConfig>,
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
@@ -58,10 +60,21 @@ pub struct Komobar {
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_config: Receiver<KomobarConfig>,
pub bg_color: Rc<RefCell<Color32>>,
pub bg_color_with_alpha: Rc<RefCell<Color32>>,
pub scale_factor: f32,
pub size_rect: komorebi_client::Rect,
applied_theme_on_first_frame: bool,
}
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
pub fn apply_theme(
ctx: &Context,
theme: KomobarTheme,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
transparency_alpha: Option<u8>,
grouping: Option<Grouping>,
render_config: Rc<RefCell<RenderConfig>>,
) {
match theme {
KomobarTheme::Catppuccin {
name: catppuccin,
@@ -141,6 +154,29 @@ pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Colo
bg_color.replace(base16.background());
}
}
// Apply transparency_alpha
let theme_color = *bg_color.borrow();
bg_color_with_alpha.replace(theme_color.try_apply_alpha(transparency_alpha));
// apply rounding to the widgets
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(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();
});
}
}
// Update RenderConfig's background_color so that widgets will have the new color
render_config.borrow_mut().background_color = *bg_color.borrow();
}
impl Komobar {
@@ -160,7 +196,136 @@ impl Komobar {
Self::add_custom_font(ctx, font_family);
}
let position = config.position.clone().unwrap_or(PositionConfig {
// Update the `size_rect` so that the bar position can be changed on the EGUI update
// function
self.update_size_rect(config.position.clone());
self.try_apply_theme(config, ctx);
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);
}
self.render_config.replace(config.new_renderconfig(
ctx,
*self.bg_color.borrow(),
config.icon_scale,
));
let mut komorebi_notification_state = previous_notification_state;
let mut komorebi_widgets = Vec::new();
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, 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_widgets.push((Komorebi::from(config), idx, Alignment::Center));
}
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Right));
}
}
let mut left_widgets = config
.left_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut center_widgets = match &config.center_widgets {
Some(center_widgets) => center_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>(),
None => vec![],
};
let mut right_widgets = config
.right_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if !komorebi_widgets.is_empty() {
komorebi_widgets
.into_iter()
.for_each(|(mut widget, idx, side)| {
match komorebi_notification_state {
None => {
komorebi_notification_state =
Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
if widget.workspaces.map_or(false, |w| w.enable) {
previous.borrow_mut().update_from_config(
&widget.komorebi_notification_state.borrow(),
);
}
widget.komorebi_notification_state = previous.clone();
}
}
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
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;
if let (Some(prev_rect), Some(new_rect)) = (
&self.config.monitor.work_area_offset,
&config.monitor.work_area_offset,
) {
if new_rect != prev_rect {
if let Err(error) = komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(config.monitor.index, *new_rect),
) {
tracing::error!(
"error applying work area offset to monitor '{}': {}",
config.monitor.index,
error,
);
} else {
tracing::info!(
"work area offset applied to monitor: {}",
config.monitor.index
);
}
}
}
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
self.config = config.clone().into();
}
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
fn update_size_rect(&mut self, position: Option<PositionConfig>) {
let position = position.unwrap_or(PositionConfig {
start: Some(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
@@ -171,42 +336,40 @@ impl Komobar {
}),
});
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 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,
});
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")
}
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())
}
}
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")
}
self.size_rect = komorebi_client::Rect {
left: start.x as i32,
top: start.y as i32,
right: end.x as i32,
bottom: end.y as i32,
};
}
fn try_apply_theme(&mut self, config: &KomobarConfig, ctx: &Context) {
match config.theme {
Some(theme) => {
apply_theme(ctx, theme, self.bg_color.clone());
apply_theme(
ctx,
theme,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
config.transparency_alpha,
config.grouping,
self.render_config.clone(),
);
}
None => {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
@@ -222,11 +385,21 @@ impl Komobar {
},
);
let bar_transparency_alpha = config.transparency_alpha;
let bar_grouping = config.grouping;
let config = home_dir.join("komorebi.json");
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), self.bg_color.clone());
apply_theme(
ctx,
KomobarTheme::from(theme),
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
bar_transparency_alpha,
bar_grouping,
self.render_config.clone(),
);
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
@@ -247,124 +420,28 @@ impl Komobar {
Err(_) => {
ctx.set_style(Style::default());
self.bg_color.replace(Style::default().visuals.panel_fill);
// apply rounding to the widgets since we didn't call `apply_theme`
if let Some(
Grouping::Bar(config)
| Grouping::Alignment(config)
| Grouping::Widget(config),
) = &bar_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();
});
}
}
}
}
}
}
// 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);
}
let mut komorebi_widget = None;
let mut komorebi_widget_idx = None;
let mut komorebi_notification_state = previous_notification_state;
let mut side = None;
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
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);
}
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Alignment::Right);
}
}
let mut left_widgets = config
.left_widgets
.iter()
.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()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if let (Some(idx), Some(mut widget), Some(side)) =
(komorebi_widget_idx, komorebi_widget, side)
{
match komorebi_notification_state {
None => {
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
previous
.borrow_mut()
.update_from_config(&widget.komorebi_notification_state.borrow());
widget.komorebi_notification_state = previous.clone();
}
}
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
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(
@@ -374,6 +451,7 @@ impl Komobar {
config: Arc<KomobarConfig>,
) -> Self {
let mut komobar = Self {
hwnd: process_hwnd(),
config: config.clone(),
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
@@ -383,7 +461,10 @@ impl Komobar {
rx_gui,
rx_config,
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
bg_color_with_alpha: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
size_rect: komorebi_client::Rect::default(),
applied_theme_on_first_frame: false,
};
komobar.apply_config(&cc.egui_ctx, &config, None);
@@ -427,7 +508,7 @@ impl Komobar {
if let Some((font, _)) = system_fonts::get(&property) {
fonts
.font_data
.insert(name.to_owned(), FontData::from_owned(font));
.insert(name.to_owned(), Arc::new(FontData::from_owned(font)));
fonts
.families
@@ -453,6 +534,10 @@ impl eframe::App for Komobar {
}
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
if self.hwnd.is_none() {
self.hwnd = process_hwnd();
}
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(
@@ -478,71 +563,135 @@ impl eframe::App for Komobar {
self.config.monitor.index,
self.rx_gui.clone(),
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme,
self.render_config.clone(),
);
}
if !self.applied_theme_on_first_frame {
self.try_apply_theme(&self.config.clone(), ctx);
self.applied_theme_on_first_frame = true;
}
// Check if egui's Window size is the expected one, if not, update it
if let Some(current_rect) = ctx.input(|i| i.viewport().outer_rect) {
// Get the correct size according to scale factor
let current_rect = komorebi_client::Rect {
left: (current_rect.min.x * self.scale_factor) as i32,
top: (current_rect.min.y * self.scale_factor) as i32,
right: ((current_rect.max.x - current_rect.min.x) * self.scale_factor) as i32,
bottom: ((current_rect.max.y - current_rect.min.y) * self.scale_factor) as i32,
};
if self.size_rect != current_rect {
if let Some(hwnd) = self.hwnd {
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&self.size_rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
}
}
let frame = if let Some(frame) = &self.config.frame {
Frame::none()
.inner_margin(Margin::symmetric(
frame.inner_margin.x,
frame.inner_margin.y,
))
.fill(*self.bg_color.borrow())
.fill(*self.bg_color_with_alpha.borrow())
} else {
Frame::none().fill(*self.bg_color.borrow())
Frame::none().fill(*self.bg_color_with_alpha.borrow())
};
let mut render_config = self.render_config.borrow_mut();
CentralPanel::default().frame(frame).show(ctx, |ui| {
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
CentralPanel::default().frame(frame).show(ctx, |_| {
// 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);
let area_frame = if let Some(frame) = &self.config.frame {
Frame::none().inner_margin(Margin::symmetric(0.0, frame.inner_margin.y))
} else {
Frame::none()
};
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
if !self.left_widgets.is_empty() {
// Left-aligned widgets layout
Area::new(Id::new("left_panel"))
.anchor(Align2::LEFT_CENTER, [0.0, 0.0]) // Align in the left center of the window
.show(ctx, |ui| {
let mut left_area_frame = area_frame;
if let Some(frame) = &self.config.frame {
left_area_frame.inner_margin.left = frame.inner_margin.x;
}
left_area_frame.show(ui, |ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
let mut render_conf = render_config.clone();
render_conf.alignment = Some(Alignment::Left);
// 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);
}
});
});
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
}
})
})
});
});
}
if !self.right_widgets.is_empty() {
// Right-aligned widgets layout
Area::new(Id::new("right_panel"))
.anchor(Align2::RIGHT_CENTER, [0.0, 0.0]) // Align in the right center of the window
.show(ctx, |ui| {
let mut right_area_frame = area_frame;
if let Some(frame) = &self.config.frame {
right_area_frame.inner_margin.right = frame.inner_margin.x;
}
right_area_frame.show(ui, |ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let mut render_conf = render_config.clone();
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| {
let center_area_frame = area_frame;
center_area_frame.show(ui, |ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
let mut render_conf = render_config.clone();
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);
}
});
});
});
});
}
});
}
}

View File

@@ -2,12 +2,11 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::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;
@@ -30,37 +29,18 @@ pub struct BatteryConfig {
impl From<BatteryConfig> for Battery {
fn from(value: BatteryConfig) -> Self {
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) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => state = Some(BatteryState::Charging),
State::Discharging => state = Some(BatteryState::Discharging),
_ => {}
}
last_state = match prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
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(),
manager: Manager::new().unwrap(),
last_state: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
state: BatteryState::Discharging,
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
@@ -124,19 +104,12 @@ impl BarWidget for Battery {
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
};
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 => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -144,7 +117,12 @@ impl BarWidget for Battery {
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {

View File

@@ -12,7 +12,7 @@ use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.31`
/// The `komorebi.bar.json` configuration file reference for `v0.1.33`
pub struct KomobarConfig {
/// Bar positioning options
#[serde(alias = "viewport")]
@@ -25,6 +25,8 @@ pub struct KomobarConfig {
pub font_family: Option<String>,
/// Font size (default: 12.5)
pub font_size: Option<f32>,
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
pub icon_scale: Option<f32>,
/// Max label width before text truncation (default: 400.0)
pub max_label_width: Option<f32>,
/// Theme
@@ -188,12 +190,16 @@ pub enum LabelPrefix {
IconAndText,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub enum DisplayFormat {
/// Show only icon
Icon,
/// Show only text
Text,
/// Show an icon and text for the selected element, and text on the rest
TextAndIconOnSelected,
/// Show both icon and text
IconAndText,
/// Show an icon and text for the selected element, and icons on the rest
IconAndTextOnSelected,
}

View File

@@ -1,13 +1,12 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
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;
@@ -30,17 +29,18 @@ pub struct CpuConfig {
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();
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
system: System::new_with_specifics(
RefreshKind::default().without_memory().without_processes(),
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
@@ -74,13 +74,6 @@ impl BarWidget for Cpu {
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 => {
@@ -88,7 +81,7 @@ impl BarWidget for Cpu {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -96,16 +89,17 @@ impl BarWidget for Cpu {
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) =

View File

@@ -1,13 +1,12 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
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 eframe::egui::WidgetText;
use schemars::JsonSchema;
@@ -90,13 +89,6 @@ impl BarWidget for Date {
if self.enable {
let mut 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 => {
@@ -104,7 +96,7 @@ impl BarWidget for Date {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -116,16 +108,22 @@ impl BarWidget for Date {
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false),
)
})
.clicked()
{
self.format.next()

View File

@@ -2,10 +2,12 @@ use crate::bar::apply_theme;
use crate::config::DisplayFormat;
use crate::config::KomobarTheme;
use crate::komorebi_layout::KomorebiLayout;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::ICON_CACHE;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_INDEX;
use crossbeam_channel::Receiver;
@@ -14,7 +16,6 @@ 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;
@@ -22,7 +23,6 @@ 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;
@@ -46,7 +46,7 @@ use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: KomorebiWorkspacesConfig,
pub workspaces: Option<KomorebiWorkspacesConfig>,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Focused Window widget
@@ -113,7 +113,10 @@ impl From<&KomorebiConfig> for Komorebi {
selected_workspace: String::new(),
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
workspaces: vec![],
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
hide_empty_workspaces: value
.workspaces
.map(|w| w.hide_empty_workspaces)
.unwrap_or_default(),
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
@@ -131,7 +134,7 @@ impl From<&KomorebiConfig> for Komorebi {
#[derive(Clone, Debug)]
pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: KomorebiWorkspacesConfig,
pub workspaces: Option<KomorebiWorkspacesConfig>,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
@@ -140,140 +143,140 @@ pub struct Komorebi {
impl BarWidget for Komorebi {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
let icon_size = Vec2::splat(config.icon_font_id.size);
if self.workspaces.enable {
let mut update = None;
if let Some(workspaces) = self.workspaces {
if workspaces.enable {
let mut update = None;
if !komorebi_notification_state.workspaces.is_empty() {
let format = self.workspaces.display.unwrap_or(DisplayFormat::Text);
if !komorebi_notification_state.workspaces.is_empty() {
let format = workspaces.display.unwrap_or(DisplayFormat::Text);
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, container_information)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
if SelectableFrame::new(
komorebi_notification_state.selected_workspace.eq(ws),
)
.show(ui, |ui| {
let mut has_icon = false;
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
let icons: Vec<_> =
container_information.icons.iter().flatten().collect();
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;
}
}
});
}
}
// 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);
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);
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()
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, container_information)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
update = Some(ws.to_string());
let mut proceed = true;
if SelectableFrame::new(
komorebi_notification_state.selected_workspace.eq(ws),
)
.show(ui, |ui| {
let mut has_icon = false;
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
if format == DisplayFormat::Icon
|| format == DisplayFormat::IconAndText
|| format == DisplayFormat::IconAndTextOnSelected
|| (format == DisplayFormat::TextAndIconOnSelected
&& komorebi_notification_state.selected_workspace.eq(ws))
{
let icons: Vec<_> =
container_information.icons.iter().flatten().collect();
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)
.fit_to_exact_size(icon_size),
);
if !has_icon {
has_icon = true;
}
}
});
}
}
// draw a custom icon when there is no app icon
if match format {
DisplayFormat::Icon => !has_icon,
_ => false,
} {
let (response, painter) =
ui.allocate_painter(icon_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);
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 if format != DisplayFormat::IconAndTextOnSelected
|| (format == DisplayFormat::IconAndTextOnSelected
&& komorebi_notification_state.selected_workspace.eq(ws))
{
ui.add(Label::new(ws.to_string()).selectable(false))
} else {
ui.response()
}
})
.clicked()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
update = Some(ws.to_string());
if proceed
&& komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
if komorebi_notification_state.mouse_follows_focus {
if komorebi_client::send_batch([
SocketMessage::MouseFollowsFocus(false),
SocketMessage::FocusMonitorWorkspaceNumber(
komorebi_notification_state.monitor_index,
i,
),
SocketMessage::RetileWithResizeDimensions,
SocketMessage::MouseFollowsFocus(true),
])
.is_err()
{
tracing::error!(
"could not send the following batch of messages to komorebi:\n
MouseFollowsFocus(false)\n
FocusMonitorWorkspaceNumber({}, {})\n
RetileWithResizeDimensions
MouseFollowsFocus(true)\n",
komorebi_notification_state.monitor_index,
i,
);
}
} else if komorebi_client::send_batch([
SocketMessage::FocusMonitorWorkspaceNumber(
komorebi_notification_state.monitor_index,
i,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusWorkspaceNumber"
);
proceed = false;
}
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");
SocketMessage::RetileWithResizeDimensions,
])
.is_err()
{
tracing::error!(
"could not send the following batch of messages to komorebi:\n
FocusMonitorWorkspaceNumber({}, {})\n
RetileWithResizeDimensions",
komorebi_notification_state.monitor_index,
i,
);
}
}
}
}
});
}
});
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
}
}
@@ -368,9 +371,10 @@ impl BarWidget for Komorebi {
.focused_window_idx;
let iter = titles.iter().zip(icons.iter());
let len = iter.len();
for (i, (title, icon)) in iter.enumerate() {
let selected = i == focused_window_idx;
let selected = i == focused_window_idx && len != 1;
if SelectableFrame::new(selected)
.show(ui, |ui| {
@@ -383,7 +387,11 @@ impl BarWidget for Komorebi {
},
);
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format
if format == DisplayFormat::Icon
|| format == DisplayFormat::IconAndText
|| format == DisplayFormat::IconAndTextOnSelected
|| (format == DisplayFormat::TextAndIconOnSelected
&& i == focused_window_idx)
{
if let Some(img) = icon {
Frame::none()
@@ -394,7 +402,7 @@ impl BarWidget for Komorebi {
let response = ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.shrink_to_fit(),
.fit_to_exact_size(icon_size),
);
if let DisplayFormat::Icon = format {
@@ -404,7 +412,11 @@ impl BarWidget for Komorebi {
}
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format
if format == DisplayFormat::Text
|| format == DisplayFormat::IconAndText
|| format == DisplayFormat::TextAndIconOnSelected
|| (format == DisplayFormat::IconAndTextOnSelected
&& i == focused_window_idx)
{
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
@@ -424,35 +436,27 @@ impl BarWidget for Komorebi {
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()
{
if komorebi_notification_state.mouse_follows_focus {
if komorebi_client::send_batch([
SocketMessage::MouseFollowsFocus(false),
SocketMessage::FocusStackWindow(i),
SocketMessage::MouseFollowsFocus(true),
]).is_err() {
tracing::error!(
"could not send the following batch of messages to komorebi:\n
MouseFollowsFocus(false)\n
FocusStackWindow({})\n
MouseFollowsFocus(true)\n",
i,
);
}
} else 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"
);
}
}
}
});
@@ -487,12 +491,18 @@ impl KomorebiNotificationState {
self.hide_empty_workspaces = config.hide_empty_workspaces;
}
#[allow(clippy::too_many_arguments)]
pub fn handle_notification(
&mut self,
ctx: &Context,
monitor_index: usize,
rx_gui: Receiver<komorebi_client::Notification>,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
transparency_alpha: Option<u8>,
grouping: Option<Grouping>,
default_theme: Option<KomobarTheme>,
render_config: Rc<RefCell<RenderConfig>>,
) {
match rx_gui.try_recv() {
Err(error) => match error {
@@ -510,13 +520,42 @@ impl KomorebiNotificationState {
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());
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from updated komorebi.json");
} else if let Some(default_theme) = default_theme {
apply_theme(
ctx,
default_theme,
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("removed theme from updated komorebi.json and applied default theme");
} else {
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
}
}
}
SocketMessage::Theme(theme) => {
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color,
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
@@ -610,17 +649,38 @@ impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
impl From<&Container> for KomorebiNotificationStateContainerInformation {
fn from(value: &Container) -> Self {
let windows = value.windows().iter().collect::<Vec<_>>();
let mut icons = vec![];
for window in windows {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let exe = window.exe().unwrap_or_default();
match icon_cache.get(&exe) {
None => {
icons.push(windows_icons::get_icon_by_process_id(window.process_id()));
update_cache = true;
}
Some(icon) => {
icons.push(Some(icon.clone()));
}
}
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
}
}
}
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<_>>(),
icons,
focused_window_idx: value.focused_window_idx(),
}
}
@@ -628,9 +688,30 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
impl From<&Window> for KomorebiNotificationStateContainerInformation {
fn from(value: &Window) -> Self {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let mut icons = vec![];
let exe = value.exe().unwrap_or_default();
match icon_cache.get(&exe) {
None => {
icons.push(windows_icons::get_icon_by_process_id(value.process_id()));
update_cache = true;
}
Some(icon) => {
icons.push(Some(icon.clone()));
}
}
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
}
}
Self {
titles: vec![value.title().unwrap_or_default()],
icons: vec![windows_icons::get_icon_by_process_id(value.process_id())],
icons,
focused_window_idx: 0,
}
}

View File

@@ -10,7 +10,6 @@ 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;
@@ -225,13 +224,7 @@ impl KomorebiLayout {
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 font_id = render_config.icon_font_id.clone();
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);

View File

@@ -13,6 +13,7 @@ mod selected_frame;
mod storage;
mod time;
mod ui;
mod update;
mod widget;
use crate::bar::Komobar;
@@ -24,9 +25,11 @@ use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use image::RgbaImage;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::collections::HashMap;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
@@ -34,6 +37,8 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
@@ -53,6 +58,9 @@ pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
@@ -344,6 +352,10 @@ fn main() -> color_eyre::Result<()> {
for client in listener.incoming() {
match client {
Ok(subscription) => {
match subscription.set_read_timeout(Some(Duration::from_secs(1))) {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
}
let mut buffer = Vec::new();
let mut reader = BufReader::new(subscription);

View File

@@ -1,14 +1,13 @@
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
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 eframe::egui::Vec2;
use schemars::JsonSchema;
@@ -82,16 +81,9 @@ impl BarWidget for Media {
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(
egui_phosphor::regular::HEADPHONES.to_string(),
font_id.clone(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -99,24 +91,28 @@ impl BarWidget for Media {
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(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(),
)
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(),
)
})
.clicked()
{
self.toggle();

View File

@@ -1,13 +1,12 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
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;
@@ -30,17 +29,18 @@ pub struct MemoryConfig {
impl From<MemoryConfig> for Memory {
fn from(value: MemoryConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
system.refresh_memory();
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
system: System::new_with_specifics(
RefreshKind::default().without_cpu().without_processes(),
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
@@ -77,13 +77,6 @@ impl BarWidget for Memory {
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 => {
@@ -91,7 +84,7 @@ impl BarWidget for Memory {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -99,16 +92,17 @@ impl BarWidget for Memory {
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) =

View File

@@ -1,13 +1,12 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
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 num_derive::FromPrimitive;
use schemars::JsonSchema;
@@ -27,6 +26,8 @@ pub struct NetworkConfig {
pub show_total_data_transmitted: bool,
/// Show network activity
pub show_network_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Characters to reserve for network activity data
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
@@ -41,12 +42,13 @@ impl From<NetworkConfig> for Network {
Self {
enable: value.enable,
show_total_activity: value.show_total_data_transmitted,
show_activity: value.show_network_activity,
show_default_interface: value.show_default_interface.unwrap_or(true),
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(),
@@ -63,6 +65,7 @@ pub struct Network {
pub enable: bool,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
@@ -97,7 +100,7 @@ impl Network {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh();
self.networks_network_activity.refresh(true);
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
@@ -135,7 +138,12 @@ impl Network {
(activity, total_activity)
}
fn reading_to_label(&self, ctx: &Context, reading: NetworkReading) -> Label {
fn reading_to_label(
&self,
ctx: &Context,
reading: NetworkReading,
config: RenderConfig,
) -> Label {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
@@ -175,16 +183,16 @@ impl Network {
},
};
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
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());
let icon_format = TextFormat::simple(
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
);
let text_format = TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
};
// icon
let mut layout_job = LayoutJob::simple(
@@ -250,74 +258,77 @@ impl Network {
impl BarWidget for Network {
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));
});
}
}
if self.show_activity {
for reading in activity {
config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading));
});
}
}
}
if self.enable {
self.default_interface();
// widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone();
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
if self.show_total_activity || self.show_activity {
let (activity, total_activity) = self.network_activity();
let mut layout_job = LayoutJob::simple(
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: ");
if self.show_total_activity {
for reading in total_activity {
render_config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone()));
});
}
}
layout_job.append(
&self.default_interface,
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", "ncpa"]).spawn() {
eprintln!("{}", error)
}
if self.show_activity {
for reading in activity {
render_config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone()));
});
}
});
}
}
if self.show_default_interface {
self.default_interface();
if !self.default_interface.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_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 {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
render_config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{}", error)
}
}
});
}
}
// widget spacing: pass on the config that was use for calling the apply_on_widget function
*config = render_config.clone();
}
}
}

View File

@@ -1,11 +1,14 @@
use crate::bar::Alignment;
use crate::config::KomobarConfig;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::InnerResponse;
use eframe::egui::Margin;
use eframe::egui::Rounding;
use eframe::egui::Shadow;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
@@ -13,6 +16,7 @@ use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
@@ -29,7 +33,7 @@ pub enum Grouping {
Widget(GroupingConfig),
}
#[derive(Copy, Clone)]
#[derive(Clone)]
pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize,
@@ -45,14 +49,38 @@ pub struct RenderConfig {
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,
/// FontId for text
pub text_font_id: FontId,
/// FontId for icon (based on scaling the text font id)
pub icon_font_id: FontId,
}
pub trait RenderExt {
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig;
fn new_renderconfig(
&self,
ctx: &Context,
background_color: Color32,
icon_scale: Option<f32>,
) -> RenderConfig;
}
impl RenderExt for &KomobarConfig {
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
fn new_renderconfig(
&self,
ctx: &Context,
background_color: Color32,
icon_scale: Option<f32>,
) -> RenderConfig {
let text_font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut icon_font_id = text_font_id.clone();
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
RenderConfig {
monitor_idx: self.monitor.index,
spacing: self.widget_spacing.unwrap_or(10.0),
@@ -61,6 +89,8 @@ impl RenderExt for &KomobarConfig {
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id,
icon_font_id,
}
}
}
@@ -83,21 +113,33 @@ impl RenderConfig {
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id: FontId::default(),
icon_font_id: FontId::default(),
}
}
pub fn apply_on_bar<R>(
pub fn change_frame_on_bar(
&mut self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
frame: Frame,
ui_style: &Arc<eframe::egui::Style>,
) -> Frame {
self.alignment = None;
if let Grouping::Bar(config) = self.grouping {
return self.define_group(None, config, ui, add_contents);
return self.define_group_frame(
//TODO: this outer margin can be a config
Some(Margin {
left: 10.0,
right: 10.0,
top: 6.0,
bottom: 6.0,
}),
config,
ui_style,
);
}
Self::fallback_group(ui, add_contents)
frame
}
pub fn apply_on_alignment<R>(
@@ -159,16 +201,26 @@ impl RenderConfig {
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
Frame::group(ui.style_mut())
self.define_group_frame(outer_margin, config, ui.style())
.show(ui, add_contents)
}
pub fn define_group_frame(
&mut self,
outer_margin: Option<Margin>,
config: GroupingConfig,
ui_style: &Arc<eframe::egui::Style>,
) -> Frame {
Frame::group(ui_style)
.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),
true => Margin::symmetric(6.0, 1.0),
false => Margin::symmetric(1.0, 1.0),
})
.stroke(ui.style().visuals.widgets.noninteractive.bg_stroke)
.stroke(ui_style.visuals.widgets.noninteractive.bg_stroke)
.rounding(match config.rounding {
Some(rounding) => rounding.into(),
None => ui.style().visuals.widgets.noninteractive.rounding,
None => ui_style.visuals.widgets.noninteractive.rounding,
})
.fill(
self.background_color
@@ -178,16 +230,60 @@ impl RenderConfig {
Some(style) => match style {
// new styles can be added if needed here
GroupingStyle::Default => Shadow::NONE,
GroupingStyle::DefaultWithShadow => Shadow {
GroupingStyle::DefaultWithShadowB4O1S3 => Shadow {
blur: 4.0,
offset: Vec2::new(1.0, 1.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithShadowB4O0S3 => Shadow {
blur: 4.0,
offset: Vec2::new(0.0, 0.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithShadowB0O1S3 => Shadow {
blur: 0.0,
offset: Vec2::new(1.0, 1.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB3O1S2 => Shadow {
blur: 3.0,
offset: Vec2::new(1.0, 1.0),
spread: 2.0,
color: ui_style
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB3O0S2 => Shadow {
blur: 3.0,
offset: Vec2::new(0.0, 0.0),
spread: 2.0,
color: ui_style
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB0O1S2 => Shadow {
blur: 0.0,
offset: Vec2::new(1.0, 1.0),
spread: 2.0,
color: ui_style
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
},
None => Shadow::NONE,
})
.show(ui, add_contents)
}
fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin {
@@ -239,9 +335,20 @@ pub struct GroupingConfig {
pub enum GroupingStyle {
#[serde(alias = "CtByte")]
Default,
/// A black shadow is added under the default group
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
#[serde(alias = "CtByteWithShadow")]
DefaultWithShadow,
#[serde(alias = "DefaultWithShadow")]
DefaultWithShadowB4O1S3,
/// A shadow is added under the default group. (blur: 4, offset: x-0 y-0, spread: 3)
DefaultWithShadowB4O0S3,
/// A shadow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 3)
DefaultWithShadowB0O1S3,
/// A glow is added under the default group. (blur: 3, offset: x-1 y-1, spread: 2)
DefaultWithGlowB3O1S2,
/// A glow is added under the default group. (blur: 3, offset: x-0 y-0, spread: 2)
DefaultWithGlowB3O0S2,
/// A glow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 2)
DefaultWithGlowB0O1S2,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]

View File

@@ -1,3 +1,4 @@
use eframe::egui::CursorIcon;
use eframe::egui::Frame;
use eframe::egui::Margin;
use eframe::egui::Response;
@@ -50,6 +51,6 @@ impl SelectableFrame {
response
})
.inner
.on_hover_cursor(eframe::egui::CursorIcon::PointingHand)
.on_hover_cursor(CursorIcon::PointingHand)
}
}

View File

@@ -1,13 +1,12 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
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;
@@ -51,7 +50,7 @@ impl Storage {
fn output(&mut self) -> Vec<String> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh();
self.disks.refresh(true);
self.last_updated = now;
}
@@ -81,13 +80,6 @@ impl Storage {
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
for output in self.output() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
@@ -96,7 +88,7 @@ impl BarWidget for Storage {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -104,16 +96,17 @@ impl BarWidget for Storage {
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("cmd.exe")

View File

@@ -1,13 +1,12 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
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;
@@ -81,13 +80,6 @@ impl BarWidget for Time {
if self.enable {
let mut 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 => {
@@ -95,7 +87,7 @@ impl BarWidget for Time {
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
@@ -107,16 +99,17 @@ impl BarWidget for Time {
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
self.format.toggle()

158
komorebi-bar/src/update.rs Normal file
View File

@@ -0,0 +1,158 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct UpdateConfig {
/// Enable the Update widget
pub enable: bool,
/// Data refresh interval (default: 12 hours)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<UpdateConfig> for Update {
fn from(value: UpdateConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(12);
let mut latest_version = String::new();
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebi-bar-version-checker")
.send()
{
#[derive(Deserialize)]
struct Release {
tag_name: String,
}
if let Ok(release) =
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
{
let trimmed = release.tag_name.trim_start_matches("v");
latest_version = trimmed.to_string();
}
}
Self {
enable: value.enable,
data_refresh_interval,
installed_version: env!("CARGO_PKG_VERSION").to_string(),
latest_version,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
pub struct Update {
pub enable: bool,
data_refresh_interval: u64,
installed_version: String,
latest_version: String,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Update {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated)
> Duration::from_secs((self.data_refresh_interval * 60) * 60)
{
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebi-bar-version-checker")
.send()
{
#[derive(Deserialize)]
struct Release {
tag_name: String,
}
if let Ok(release) =
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
{
let trimmed = release.tag_name.trim_start_matches("v");
self.latest_version = trimmed.to_string();
}
}
self.last_updated = now;
}
if self.latest_version > self.installed_version {
format!("Update available! v{}", self.latest_version)
} else {
String::new()
}
}
}
impl BarWidget for Update {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ROCKET_LAUNCH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("explorer.exe")
.args([format!(
"https://github.com/LGUG2Z/komorebi/releases/v{}",
self.latest_version
)])
.spawn()
{
eprintln!("{}", error)
}
}
});
}
}
}
}

View File

@@ -17,6 +17,8 @@ use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use crate::update::Update;
use crate::update::UpdateConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
@@ -38,6 +40,7 @@ pub enum WidgetConfig {
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
Update(UpdateConfig),
}
impl WidgetConfig {
@@ -52,6 +55,30 @@ impl WidgetConfig {
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
}
}
pub fn enabled(&self) -> bool {
match self {
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().map_or(false, |w| w.enable)
|| config.layout.as_ref().map_or(false, |w| w.enable)
|| config.focused_window.as_ref().map_or(false, |w| w.enable)
|| config
.configuration_switcher
.as_ref()
.map_or(false, |w| w.enable)
}
WidgetConfig::Media(config) => config.enable,
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
WidgetConfig::Update(config) => config.enable,
}
}
}

View File

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

View File

@@ -68,6 +68,20 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
pub fn send_batch(messages: impl IntoIterator<Item = 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)))?;
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
if let Ok(m_str) = serde_json::to_string(&m) {
s.push_str(&m_str);
s.push('\n');
}
s
});
stream.write_all(msgs.as_bytes())
}
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);

View File

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

View File

@@ -1,12 +1,12 @@
[package]
name = "komorebi-themes"
version = "0.1.31"
version = "0.1.33"
edition = "2021"
[dependencies]
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"] }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "24362c4" }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f85cc3c", default-features = false, features = ["egui30"] }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
eframe = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }

View File

@@ -1,8 +1,7 @@
[package]
name = "komorebi"
version = "0.1.31"
version = "0.1.33"
description = "A tiling window manager for Windows"
categories = ["tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
@@ -48,7 +47,7 @@ windows-core = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.52"
winreg = "0.53"
[build-dependencies]
shadow-rs = { workspace = true }

View File

@@ -1,3 +1,5 @@
use shadow_rs::ShadowBuilder;
fn main() {
shadow_rs::new().unwrap();
ShadowBuilder::builder().build().unwrap();
}

View File

@@ -425,6 +425,18 @@ impl Border {
return LRESULT(0);
}
let reference_hwnd = (*border_pointer).tracking_hwnd;
// Update position to update the ZOrder
let border_window_rect = (*border_pointer).window_rect;
tracing::trace!("updating border position");
if let Err(error) =
(*border_pointer).set_position(&border_window_rect, reference_hwnd)
{
tracing::error!("failed to update border position {error}");
}
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);

View File

@@ -169,6 +169,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
drop(state);
@@ -238,10 +239,19 @@ 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())
{
// when we switch focus to/from a floating window
let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
// if we switch focus to a floating window
fw == &notification.0.unwrap_or_default() ||
// if there is any floating window with a `WindowKind::Floating` border
// that no longer is the foreground window then we need to update that
// border.
(fw != &foreground_window
&& window_border(*fw)
.map(|b| b.window_kind == WindowKind::Floating)
.unwrap_or_default())
});
if !should_process_notification && switch_focus_to_from_floating_window {
should_process_notification = true;
}
@@ -321,22 +331,15 @@ 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 new_focus_state = if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
};
border.window_kind = new_focus_state;
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(
border.hwnd,
if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
},
);
focus_state.insert(border.hwnd, new_focus_state);
}
let reference_hwnd =
@@ -350,6 +353,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
border.invalidate();
borders_monitors.insert(monocle.id().clone(), monitor_idx);
windows_borders.insert(
monocle.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
for (id, b) in borders.iter() {
@@ -369,9 +378,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![];
@@ -416,7 +427,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
borders.remove(id);
}
for (idx, c) in ws.containers().iter().enumerate() {
'containers: for (idx, c) in ws.containers().iter().enumerate() {
// Get the border entry for this container from the map or create one
let mut new_border = false;
let border = match borders.entry(c.id().clone()) {
@@ -434,17 +445,14 @@ 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;
let new_focus_state = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
|| c.focused_window()
.map(|w| w.hwnd != foreground_window)
.unwrap_or_default()
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
@@ -452,6 +460,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} else {
WindowKind::Single
};
border.window_kind = new_focus_state;
// Update the focused state for all containers on this workspace
{
@@ -462,7 +471,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let reference_hwnd =
c.focused_window().copied().unwrap_or_default().hwnd;
let rect = WindowsApi::window_rect(reference_hwnd)?;
// avoid getting into a thread restart loop if we try to look up
// rect info for a window that has been destroyed by the time
// we get here
let rect = match WindowsApi::window_rect(reference_hwnd) {
Ok(rect) => rect,
Err(_) => {
let _ = border.destroy();
borders.remove(c.id());
continue 'containers;
}
};
let should_invalidate = match last_focus_state {
None => true,
@@ -476,10 +495,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if should_invalidate {
border.invalidate();
}
borders_monitors.insert(c.id().clone(), monitor_idx);
windows_borders.insert(
c.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
}
{
'windows: for window in ws.floating_windows() {
for window in ws.floating_windows() {
let mut new_border = false;
let border = match borders.entry(window.hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
@@ -495,33 +520,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
windows_borders.insert(window.hwnd, border.clone());
let mut should_destroy = false;
if let Some(notification_hwnd) = notification.0 {
if notification_hwnd != window.hwnd {
should_destroy = true;
}
}
if WindowsApi::foreground_window().unwrap_or_default()
!= window.hwnd
{
should_destroy = true;
}
if should_destroy {
border.destroy()?;
borders.remove(&window.hwnd.to_string());
borders_monitors.remove(&window.hwnd.to_string());
continue 'windows;
}
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = WindowKind::Floating;
let mut new_focus_state = WindowKind::Unfocused;
if foreground_window == window.hwnd {
new_focus_state = WindowKind::Floating;
}
border.window_kind = new_focus_state;
{
let mut focus_state = FOCUS_STATE.lock();
last_focus_state =
@@ -542,6 +549,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if should_invalidate {
border.invalidate();
}
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
windows_borders.insert(window.hwnd, border.clone());
}
}
}

View File

@@ -75,6 +75,18 @@ impl Container {
None
}
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
for (idx, window) in self.windows().iter().enumerate() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(idx);
}
}
}
None
}
pub fn contains_window(&self, hwnd: isize) -> bool {
for window in self.windows() {
if window.hwnd == hwnd {

View File

@@ -77,6 +77,7 @@ pub enum SocketMessage {
Promote,
PromoteFocus,
PromoteWindow(OperationDirection),
EagerFocus(String),
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
@@ -105,6 +106,7 @@ pub enum SocketMessage {
NewWorkspace,
ToggleTiling,
Stop,
StopIgnoreRestore,
TogglePause,
Retile,
RetileWithResizeDimensions,
@@ -184,6 +186,7 @@ pub enum SocketMessage {
ClearWorkspaceRules(usize, usize),
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
EnforceWorkspaceRules,
#[serde(alias = "FloatRule")]
IgnoreRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
@@ -235,7 +238,9 @@ pub struct SubscribeOptions {
pub filter_state_changes: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema, ValueEnum,
)]
pub enum StackbarMode {
Always,
Never,

View File

@@ -215,7 +215,7 @@ lazy_static! {
// 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 NO_TITLEBAR: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -298,6 +298,7 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
| NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(_))
| NotificationEvent::WindowManager(WindowManagerEvent::TitleUpdate(_, _))
| NotificationEvent::WindowManager(WindowManagerEvent::Show(_, _))
| NotificationEvent::WindowManager(WindowManagerEvent::Uncloak(_, _))
);
let notification = &serde_json::to_string(&notification)?;

View File

@@ -7,6 +7,7 @@
clippy::doc_markdown
)]
use std::env::temp_dir;
use std::net::Shutdown;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
@@ -43,6 +44,7 @@ use komorebi::stackbar_manager;
use komorebi::static_config::StaticConfig;
use komorebi::theme_manager;
use komorebi::transparency_manager;
use komorebi::window_manager::State;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
use komorebi::winevent_listener;
@@ -156,6 +158,9 @@ struct Opts {
/// Path to a static configuration JSON file
#[clap(short, long)]
config: Option<PathBuf>,
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
#[clap(long)]
clean_state: bool,
}
#[tracing::instrument]
@@ -172,7 +177,7 @@ fn main() -> Result<()> {
SESSION_ID.store(session_id, Ordering::SeqCst);
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All);
system.refresh_processes(ProcessesToUpdate::All, true);
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
@@ -260,6 +265,13 @@ fn main() -> Result<()> {
}
}
let dumped_state = temp_dir().join("komorebi.state.json");
if !opts.clean_state && dumped_state.is_file() {
let state: State = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?)?;
wm.lock().apply_state(state);
}
wm.lock().retile_all(false)?;
listen_for_events(wm.clone());
@@ -290,9 +302,12 @@ fn main() -> Result<()> {
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
let state = State::from(&*wm.lock());
std::fs::write(dumped_state, serde_json::to_string_pretty(&state)?)?;
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
wm.lock().restore_all_windows()?;
wm.lock().restore_all_windows(false)?;
AnimationEngine::wait_for_all_animations();
if WindowsApi::focus_follows_mouse()? {

View File

@@ -4,7 +4,6 @@ use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::net::Shutdown;
use std::net::TcpListener;
use std::net::TcpStream;
use std::num::NonZeroUsize;
@@ -22,7 +21,6 @@ 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;
@@ -67,6 +65,7 @@ use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::WorkspaceWindowLocation;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
@@ -231,6 +230,65 @@ impl WindowManager {
self.focus_container_in_direction(direction)?;
self.promote_container_to_front()?
}
SocketMessage::EagerFocus(ref exe) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx()?;
let mut window_location = None;
let mut monitor_workspace_indices = None;
'search: for (monitor_idx, monitor) in self.monitors().iter().enumerate() {
for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() {
if let Some(location) = workspace.location_from_exe(exe) {
window_location = Some(location);
monitor_workspace_indices = Some((monitor_idx, workspace_idx));
break 'search;
}
}
}
if let Some((monitor_idx, workspace_idx)) = monitor_workspace_indices {
if monitor_idx != focused_monitor_idx {
self.focus_monitor(monitor_idx)?;
}
if workspace_idx != focused_workspace_idx {
self.focus_workspace(workspace_idx)?;
}
}
if let Some(location) = window_location {
match location {
WorkspaceWindowLocation::Monocle(window_idx) => {
self.focus_container_window(window_idx)?;
}
WorkspaceWindowLocation::Maximized => {
if let Some(window) =
self.focused_workspace_mut()?.maximized_window_mut()
{
window.focus(self.mouse_follows_focus)?;
}
}
WorkspaceWindowLocation::Container(container_idx, window_idx) => {
let focused_container_idx = self.focused_container_idx()?;
if container_idx != focused_container_idx {
self.focused_workspace_mut()?.focus_container(container_idx);
}
self.focus_container_window(window_idx)?;
}
WorkspaceWindowLocation::Floating(window_idx) => {
if let Some(window) = self
.focused_workspace_mut()?
.floating_windows_mut()
.get_mut(window_idx)
{
window.focus(self.mouse_follows_focus)?;
}
}
}
}
}
SocketMessage::FocusWindow(direction) => {
self.focus_container_in_direction(direction)?;
}
@@ -398,6 +456,13 @@ impl WindowManager {
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
workspace_rules.clear();
}
SocketMessage::EnforceWorkspaceRules => {
{
let mut already_moved = self.already_moved_window_handles.lock();
already_moved.clear();
}
self.enforce_workspace_rules()?;
}
SocketMessage::ManageRule(identifier, ref id) => {
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
@@ -812,9 +877,7 @@ impl WindowManager {
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));
let next_focused_workspace_idx = focused_workspace_idx.saturating_sub(1);
if let Some(workspace) = monitor.focused_workspace() {
if monitor.workspaces().len() > 1
@@ -834,7 +897,7 @@ impl WindowManager {
.remove(focused_workspace_idx)
.is_some()
{
self.focus_workspace(last_focused_workspace)?;
self.focus_workspace(next_focused_workspace_idx)?;
}
}
}
@@ -912,30 +975,10 @@ impl WindowManager {
}
}
SocketMessage::Stop => {
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()?;
}
let sockets = SUBSCRIPTION_SOCKETS.lock();
for path in (*sockets).values() {
if let Ok(stream) = UnixStream::connect(path) {
stream.shutdown(Shutdown::Both)?;
}
}
let socket = DATA_DIR.join("komorebi.sock");
let _ = std::fs::remove_file(socket);
std::process::exit(0)
self.stop(false)?;
}
SocketMessage::StopIgnoreRestore => {
self.stop(true)?;
}
SocketMessage::MonitorIndexPreference(index_preference, left, top, right, bottom) => {
let mut monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
@@ -1250,7 +1293,7 @@ impl WindowManager {
// Pause so that restored windows come to the foreground from all workspaces
self.is_paused = true;
// Bring all windows to the foreground
self.restore_all_windows()?;
self.restore_all_windows(false)?;
// Create a new wm from the config path
let mut wm = StaticConfig::preload(
@@ -1619,6 +1662,7 @@ impl WindowManager {
}
SocketMessage::StackbarMode(mode) => {
STACKBAR_MODE.store(mode);
self.retile_all(true)?;
}
SocketMessage::StackbarLabel(label) => {
STACKBAR_LABEL.store(label);
@@ -1684,10 +1728,24 @@ impl WindowManager {
reply.write_all(config.as_bytes())?;
}
SocketMessage::RemoveTitleBar(_, ref id) => {
SocketMessage::RemoveTitleBar(identifier, ref id) => {
let mut identifiers = NO_TITLEBAR.lock();
if !identifiers.contains(id) {
identifiers.push(id.clone());
let mut should_push = true;
for i in &*identifiers {
if let MatchingRule::Simple(i) = i {
if i.id.eq(id) {
should_push = false;
}
}
}
if should_push {
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
kind: identifier,
id: id.clone(),
matching_strategy: Option::from(MatchingStrategy::Legacy),
}));
}
}
SocketMessage::ToggleTitleBars => {

View File

@@ -353,10 +353,11 @@ impl WindowManager {
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() {
let regex_identifiers = REGEX_IDENTIFIERS.lock();
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
(window.title(), window.exe(), window.class(), window.path())
{

View File

@@ -34,6 +34,8 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
let mut wm = arc.lock();
let offset = wm.work_area_offset;
let mut update_borders = false;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let work_area = *monitor.work_area_size();
let window_based_work_area_offset = (
@@ -51,7 +53,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(None);
update_borders = true;
tracing::info!(
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
reaped_orphans.0,
@@ -62,5 +64,9 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
}
}
}
if update_borders {
border_manager::send_notification(None);
}
}
}

View File

@@ -35,6 +35,7 @@ use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::Axis;
use crate::CrossBoundaryBehaviour;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
@@ -46,6 +47,7 @@ use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
@@ -147,6 +149,9 @@ pub struct WorkspaceConfig {
/// (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_override: Option<bool>,
/// Specify an axis on which to flip the selected layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_flip: Option<Axis>,
}
impl From<&Workspace> for WorkspaceConfig {
@@ -201,6 +206,7 @@ impl From<&Workspace> for WorkspaceConfig {
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(),
layout_flip: value.layout_flip(),
}
}
}
@@ -237,7 +243,7 @@ impl From<&Monitor> for MonitorConfig {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.json` static configuration file reference for `v0.1.31`
/// The `komorebi.json` static configuration file reference for `v0.1.33`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -373,6 +379,9 @@ pub struct StaticConfig {
#[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>>,
/// HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars
#[serde(skip_serializing_if = "Option::is_none")]
pub remove_titlebar_applications: Option<Vec<MatchingRule>>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -627,6 +636,7 @@ impl From<&WindowManager> for StaticConfig {
),
slow_application_identifiers: Option::from(SLOW_APPLICATION_IDENTIFIERS.lock().clone()),
bar_configurations: None,
remove_titlebar_applications: Option::from(NO_TITLEBAR.lock().clone()),
}
}
}
@@ -773,6 +783,7 @@ impl StaticConfig {
let mut transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
let mut slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
let mut floating_applications = FLOATING_APPLICATIONS.lock();
let mut no_titlebar_applications = NO_TITLEBAR.lock();
if let Some(rules) = &mut self.ignore_rules {
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
@@ -818,6 +829,10 @@ impl StaticConfig {
)?;
}
if let Some(rules) = &mut self.remove_titlebar_applications {
populate_rules(rules, &mut no_titlebar_applications, &mut regex_identifiers)?;
}
if let Some(stackbar) = &self.stackbar {
if let Some(height) = &stackbar.height {
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);

View File

@@ -784,7 +784,7 @@ pub struct RuleDebug {
pub matches_layered_whitelist: Option<MatchingRule>,
pub matches_floating_applications: Option<MatchingRule>,
pub matches_wsl2_gui: Option<String>,
pub matches_no_titlebar: Option<String>,
pub matches_no_titlebar: Option<MatchingRule>,
}
#[allow(clippy::too_many_arguments)]
@@ -889,9 +889,19 @@ fn window_is_eligible(
allow
};
let allow_titlebar_removed = {
let titlebars_removed = NO_TITLEBAR.lock();
titlebars_removed.contains(exe_name)
let titlebars_removed = NO_TITLEBAR.lock();
let allow_titlebar_removed = if let Some(rule) = should_act(
title,
exe_name,
class,
path,
&titlebars_removed,
&regex_identifiers,
) {
debug.matches_no_titlebar = Some(rule);
true
} else {
false
};
{

View File

@@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::env::temp_dir;
use std::io::ErrorKind;
use std::net::Shutdown;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
@@ -20,7 +22,11 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
use crate::animation::AnimationEngine;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::core::config_generation::MatchingRule;
use crate::core::custom_layout::CustomLayout;
use crate::core::Arrangement;
@@ -50,6 +56,7 @@ use crate::current_virtual_desktop;
use crate::load_configuration;
use crate::monitor::Monitor;
use crate::ring::Ring;
use crate::should_act;
use crate::should_act_individual;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_LABEL;
@@ -60,6 +67,8 @@ use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::static_config::StaticConfig;
use crate::transparency_manager;
use crate::transparency_manager::TRANSPARENCY_ALPHA;
use crate::transparency_manager::TRANSPARENCY_ENABLED;
use crate::window::Window;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
@@ -82,6 +91,8 @@ use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REGEX_IDENTIFIERS;
use crate::REMOVE_TITLEBARS;
use crate::SUBSCRIPTION_SOCKETS;
use crate::TRANSPARENCY_BLACKLIST;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WORKSPACE_MATCHING_RULES;
@@ -186,6 +197,9 @@ pub struct GlobalState {
pub stackbar_tab_background_colour: Colour,
pub stackbar_tab_width: i32,
pub stackbar_height: i32,
pub transparency_enabled: bool,
pub transparency_alpha: u8,
pub transparency_blacklist: Vec<MatchingRule>,
pub remove_titlebars: bool,
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Vec<MatchingRule>,
@@ -239,6 +253,9 @@ impl Default for GlobalState {
)),
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
transparency_enabled: TRANSPARENCY_ENABLED.load(Ordering::SeqCst),
transparency_alpha: TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
transparency_blacklist: TRANSPARENCY_BLACKLIST.lock().clone(),
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
@@ -353,6 +370,151 @@ impl WindowManager {
WindowsApi::load_workspace_information(&mut self.monitors)
}
#[tracing::instrument(skip(self, state))]
pub fn apply_state(&mut self, state: State) {
let mut can_apply = true;
let state_monitors_len = state.monitors.elements().len();
let current_monitors_len = self.monitors.elements().len();
if state_monitors_len != current_monitors_len {
tracing::warn!(
"cannot apply state from {}; state file has {state_monitors_len} monitors, but only {current_monitors_len} are currently connected",
temp_dir().join("komorebi.state.json").to_string_lossy()
);
return;
}
for monitor in state.monitors.elements() {
for workspace in monitor.workspaces() {
for container in workspace.containers() {
for window in container.windows() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
}
if let Some(window) = workspace.maximized_window() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
}
for window in workspace.floating_windows() {
if window.exe().is_err() {
can_apply = false;
break;
}
}
}
}
if can_apply {
tracing::info!(
"applying state from {}",
temp_dir().join("komorebi.state.json").to_string_lossy()
);
let offset = self.work_area_offset;
let mouse_follows_focus = self.mouse_follows_focus;
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
let mut focused_workspace = 0;
for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) {
if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
{
// to make sure padding changes get applied for users after a quick restart
let container_padding = workspace.container_padding();
let workspace_padding = workspace.workspace_padding();
*workspace = state_workspace.clone();
workspace.set_container_padding(container_padding);
workspace.set_workspace_padding(workspace_padding);
if state_monitor.focused_workspace_idx() == workspace_idx {
focused_workspace = workspace_idx;
}
}
}
}
if let Err(error) = monitor.focus_workspace(focused_workspace) {
tracing::warn!(
"cannot focus workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = monitor.load_focused_workspace(mouse_follows_focus) {
tracing::warn!(
"cannot load focused workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = monitor.update_focused_workspace(offset) {
tracing::warn!(
"cannot update workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
}
let focused_monitor_idx = state.monitors.focused_idx();
let focused_workspace_idx = state
.monitors
.elements()
.get(focused_monitor_idx)
.map(|m| m.focused_workspace_idx())
.unwrap_or_default();
if let Err(error) = self.focus_monitor(focused_monitor_idx) {
tracing::warn!(
"cannot focus monitor '{focused_monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = self.focus_workspace(focused_workspace_idx) {
tracing::warn!(
"cannot focus workspace '{focused_workspace_idx}' on monitor '{focused_monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
if let Err(error) = self.update_focused_workspace(true, true) {
tracing::warn!(
"cannot update focused workspace '{focused_workspace_idx}' on monitor '{focused_monitor_idx}' from {}: {}",
temp_dir().join("komorebi.state.json").to_string_lossy(),
error,
);
}
} else {
tracing::warn!(
"cannot apply state from {}; some windows referenced in the state file no longer exist",
temp_dir().join("komorebi.state.json").to_string_lossy()
);
}
}
#[tracing::instrument]
pub fn reload_configuration() {
tracing::info!("reloading configuration");
@@ -569,75 +731,80 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
.focused_workspace_idx();
let workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
// Go through all the monitors and workspaces
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
// And all the visible windows (at the top of a container)
for window in workspace.visible_windows().into_iter().flatten() {
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
let exe_name = window.exe()?;
let title = window.title()?;
let class = window.class()?;
let path = window.path()?;
// scope mutex locks to avoid deadlock if should_update_focused_workspace evaluates to true
// at the end of this function
{
let workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
// Go through all the monitors and workspaces
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
// And all the visible windows (at the top of a container)
for window in workspace.visible_windows().into_iter().flatten() {
let mut already_moved_window_handles =
self.already_moved_window_handles.lock();
for rule in &*workspace_matching_rules {
let matched = match &rule.matching_rule {
MatchingRule::Simple(r) => should_act_individual(
&title,
&exe_name,
&class,
&path,
r,
&regex_identifiers,
),
MatchingRule::Composite(r) => {
let mut composite_results = vec![];
for identifier in r {
composite_results.push(should_act_individual(
if let (Ok(exe_name), Ok(title), Ok(class), Ok(path)) =
(window.exe(), window.title(), window.class(), window.path())
{
for rule in &*workspace_matching_rules {
let matched = match &rule.matching_rule {
MatchingRule::Simple(r) => should_act_individual(
&title,
&exe_name,
&class,
&path,
identifier,
r,
&regex_identifiers,
));
),
MatchingRule::Composite(r) => {
let mut composite_results = vec![];
for identifier in r {
composite_results.push(should_act_individual(
&title,
&exe_name,
&class,
&path,
identifier,
&regex_identifiers,
));
}
composite_results.iter().all(|&x| x)
}
};
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);
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
} else {
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
}
composite_results.iter().all(|&x| x)
}
};
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);
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
} else {
self.add_window_handle_to_move_based_on_workspace_rule(
&window.title()?,
window.hwnd,
i,
j,
rule.monitor_index,
rule.workspace_index,
floating,
&mut to_move,
);
}
}
}
@@ -1242,10 +1409,45 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn restore_all_windows(&mut self) -> Result<()> {
pub fn stop(&mut self, ignore_restore: bool) -> Result<()> {
tracing::info!(
"received stop command, restoring all hidden windows and terminating process"
);
let state = &State::from(&*self);
std::fs::write(
temp_dir().join("komorebi.state.json"),
serde_json::to_string_pretty(&state)?,
)?;
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
self.restore_all_windows(ignore_restore)?;
AnimationEngine::wait_for_all_animations();
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
}
let sockets = SUBSCRIPTION_SOCKETS.lock();
for path in (*sockets).values() {
if let Ok(stream) = UnixStream::connect(path) {
stream.shutdown(Shutdown::Both)?;
}
}
let socket = DATA_DIR.join("komorebi.sock");
let _ = std::fs::remove_file(socket);
std::process::exit(0)
}
#[tracing::instrument(skip(self))]
pub fn restore_all_windows(&mut self, ignore_restore: bool) -> Result<()> {
tracing::info!("restoring all hidden windows");
let no_titlebar = NO_TITLEBAR.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let known_transparent_hwnds = transparency_manager::known_hwnds();
let border_implementation = border_manager::IMPLEMENTATION.load();
@@ -1261,7 +1463,17 @@ impl WindowManager {
for containers in workspace.containers_mut() {
for window in containers.windows_mut() {
if no_titlebar.contains(&window.exe()?) {
let should_remove_titlebar_for_window = should_act(
&window.title().unwrap_or_default(),
&window.exe().unwrap_or_default(),
&window.class().unwrap_or_default(),
&window.path().unwrap_or_default(),
&no_titlebar,
&regex_identifiers,
)
.is_some();
if should_remove_titlebar_for_window {
window.add_title_bar()?;
}
@@ -1273,7 +1485,9 @@ impl WindowManager {
window.remove_accent()?;
}
window.restore();
if !ignore_restore {
window.restore();
}
}
}
}
@@ -2047,7 +2261,7 @@ impl WindowManager {
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 {
if len.get() == 1 && idx != 0 {
bail!("there is only one window in this container");
}
@@ -3072,6 +3286,10 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no container"))
}
pub fn focused_container_idx(&self) -> Result<usize> {
Ok(self.focused_workspace()?.focused_container_idx())
}
pub fn focused_container_mut(&mut self) -> Result<&mut Container> {
self.focused_workspace_mut()?
.focused_container_mut()

View File

@@ -24,6 +24,7 @@ use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::container::Container;
use crate::ring::Ring;
use crate::should_act;
use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
use crate::static_config::WorkspaceConfig;
@@ -35,6 +36,7 @@ use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::INITIAL_CONFIGURATION_LOADED;
use crate::NO_TITLEBAR;
use crate::REGEX_IDENTIFIERS;
use crate::REMOVE_TITLEBARS;
#[allow(clippy::struct_field_names)]
@@ -117,16 +119,28 @@ impl Default for Workspace {
}
}
#[derive(Debug)]
pub enum WorkspaceWindowLocation {
Monocle(usize), // window_idx
Maximized,
Container(usize, usize), // container_idx, window_idx
Floating(usize), // idx in floating_windows
}
impl Workspace {
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> Result<()> {
self.name = Option::from(config.name.clone());
if config.container_padding.is_some() {
self.set_container_padding(config.container_padding);
} else {
self.set_container_padding(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
}
if config.workspace_padding.is_some() {
self.set_workspace_padding(config.workspace_padding);
} else {
self.set_container_padding(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
}
if let Some(layout) = &config.layout {
@@ -177,6 +191,10 @@ impl Workspace {
self.set_float_override(config.float_override);
}
if config.layout_flip.is_some() {
self.set_layout_flip(config.layout_flip);
}
Ok(())
}
@@ -354,6 +372,7 @@ impl Workspace {
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
let no_titlebar = NO_TITLEBAR.lock().clone();
let regex_identifiers = REGEX_IDENTIFIERS.lock().clone();
let container_padding = self.container_padding().unwrap_or(0);
let containers = self.containers_mut();
@@ -383,9 +402,19 @@ impl Workspace {
.focused_window()
.is_some_and(|w| w.hwnd == window.hwnd)
{
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
let should_remove_titlebar_for_window = should_act(
&window.title().unwrap_or_default(),
&window.exe().unwrap_or_default(),
&window.class().unwrap_or_default(),
&window.path().unwrap_or_default(),
&no_titlebar,
&regex_identifiers,
)
.is_some();
if should_remove_titlebars && should_remove_titlebar_for_window {
window.remove_title_bar()?;
} else if no_titlebar.contains(&window.exe()?) {
} else if should_remove_titlebar_for_window {
window.add_title_bar()?;
}
@@ -566,6 +595,41 @@ impl Workspace {
None
}
pub fn location_from_exe(&self, exe: &str) -> Option<WorkspaceWindowLocation> {
for (container_idx, container) in self.containers().iter().enumerate() {
if let Some(window_idx) = container.idx_from_exe(exe) {
return Some(WorkspaceWindowLocation::Container(
container_idx,
window_idx,
));
}
}
if let Some(window) = self.maximized_window() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Some(WorkspaceWindowLocation::Maximized);
}
}
}
if let Some(container) = self.monocle_container() {
if let Some(window_idx) = container.idx_from_exe(exe) {
return Some(WorkspaceWindowLocation::Monocle(window_idx));
}
}
for (window_idx, window) in self.floating_windows().iter().enumerate() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Some(WorkspaceWindowLocation::Floating(window_idx));
}
}
}
None
}
pub fn contains_managed_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {

View File

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

View File

@@ -1,8 +1,7 @@
[package]
name = "komorebic"
version = "0.1.31"
version = "0.1.33"
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
categories = ["cli", "tiling-window-manager", "windows"]
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

@@ -1,3 +1,5 @@
use shadow_rs::ShadowBuilder;
fn main() {
if std::fs::metadata("applications.json").is_err() {
let applications_json = reqwest::blocking::get(
@@ -6,5 +8,5 @@ fn main() {
std::fs::write("applications.json", applications_json).unwrap();
}
shadow_rs::new().unwrap();
ShadowBuilder::builder().build().unwrap();
}

View File

@@ -35,6 +35,7 @@ use miette::SourceSpan;
use paste::paste;
use schemars::gen::SchemaSettings;
use schemars::schema_for;
use serde::Deserialize;
use sysinfo::ProcessesToUpdate;
use which::which;
use windows::Win32::Foundation::HWND;
@@ -720,6 +721,13 @@ struct BorderImplementation {
style: komorebi_client::BorderImplementation,
}
#[derive(Parser)]
struct StackbarMode {
/// Desired stackbar mode
#[clap(value_enum)]
mode: komorebi_client::StackbarMode,
}
#[derive(Parser)]
struct Animation {
#[clap(value_enum)]
@@ -782,6 +790,9 @@ struct Start {
/// Start masir in a background process for focus-follows-mouse
#[clap(long)]
masir: bool,
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
#[clap(long)]
clean_state: bool,
}
#[derive(Parser)]
@@ -798,6 +809,9 @@ struct Stop {
/// Stop masir if it is running as a background process
#[clap(long)]
masir: bool,
/// Do not restore windows after stopping komorebi
#[clap(long, hide = true)]
ignore_restore: bool,
}
#[derive(Parser)]
@@ -915,12 +929,25 @@ struct EnableAutostart {
masir: bool,
}
#[derive(Parser)]
struct Check {
/// Path to a static configuration JSON file
#[clap(action, short, long)]
komorebi_config: Option<PathBuf>,
}
#[derive(Parser)]
struct ReplaceConfiguration {
/// Static configuration JSON file from which the configuration should be loaded
path: PathBuf,
}
#[derive(Parser)]
struct EagerFocus {
/// Case-sensitive exe identifier
exe: String,
}
#[derive(Parser)]
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
struct Opts {
@@ -941,7 +968,7 @@ enum SubCommand {
/// Kill background processes started by komorebic
Kill(Kill),
/// Check komorebi configuration and related files for common errors
Check,
Check(Check),
/// Show the path to komorebi.json
#[clap(alias = "config")]
Configuration,
@@ -1014,6 +1041,9 @@ enum SubCommand {
/// Move the focused window in the specified cycle direction
#[clap(arg_required_else_help = true)]
CycleMove(CycleMove),
/// Focus the first managed window matching the given exe
#[clap(arg_required_else_help = true)]
EagerFocus(EagerFocus),
/// Stack the focused window in the specified direction
#[clap(arg_required_else_help = true)]
Stack(Stack),
@@ -1303,6 +1333,8 @@ enum SubCommand {
ClearNamedWorkspaceRules(ClearNamedWorkspaceRules),
/// Remove all application association rules for all workspaces
ClearAllWorkspaceRules,
/// Enforce all workspace rules, including initial workspace rules that have already been applied
EnforceWorkspaceRules,
/// Identify an application that sends EVENT_OBJECT_NAMECHANGE on launch
#[clap(arg_required_else_help = true)]
IdentifyObjectNameChangeApplication(IdentifyObjectNameChangeApplication),
@@ -1343,6 +1375,9 @@ enum SubCommand {
/// Set the border implementation
#[clap(arg_required_else_help = true)]
BorderImplementation(BorderImplementation),
/// Set the stackbar mode
#[clap(arg_required_else_help = true)]
StackbarMode(StackbarMode),
/// Enable or disable transparency for unfocused windows
#[clap(arg_required_else_help = true)]
Transparency(Transparency),
@@ -1534,7 +1569,7 @@ fn main() -> Result<()> {
arguments.push_str(" --ahk");
}
if args.bar {
if args.masir {
arguments.push_str(" --masir");
}
@@ -1558,7 +1593,7 @@ fn main() -> Result<()> {
std::fs::remove_file(shortcut_file)?;
}
}
SubCommand::Check => {
SubCommand::Check(args) => {
let home_display = HOME_DIR.display();
if HAS_CUSTOM_CONFIG_HOME.load(Ordering::SeqCst) {
println!("KOMOREBI_CONFIG_HOME detected: {home_display}\n");
@@ -1573,7 +1608,15 @@ fn main() -> Result<()> {
println!("Looking for configuration files in {home_display}\n");
let static_config = HOME_DIR.join("komorebi.json");
let static_config = if let Some(static_config) = args.komorebi_config {
println!(
"Using an arbitrary configuration file passed to --komorebi-config flag\n"
);
static_config
} else {
HOME_DIR.join("komorebi.json")
};
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");
let config_whkd = WHKD_CONFIG_DIR.join("whkdrc");
@@ -1650,6 +1693,30 @@ fn main() -> Result<()> {
println!("No komorebi configuration found in {home_display}\n");
println!("If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration\n");
}
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebic-version-checker")
.send()
{
let version = env!("CARGO_PKG_VERSION");
#[derive(Deserialize)]
struct Release {
tag_name: String,
}
if let Ok(release) =
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
{
let trimmed = release.tag_name.trim_start_matches("v");
if trimmed > version {
println!("An updated version of komorebi is available! https://github.com/LGUG2Z/komorebi/releases/v{trimmed}");
}
}
}
}
SubCommand::Configuration => {
let static_config = HOME_DIR.join("komorebi.json");
@@ -1718,6 +1785,9 @@ fn main() -> Result<()> {
SubCommand::CycleMove(arg) => {
send_message(&SocketMessage::CycleMoveWindow(arg.cycle_direction))?;
}
SubCommand::EagerFocus(arg) => {
send_message(&SocketMessage::EagerFocus(arg.exe))?;
}
SubCommand::MoveToMonitor(arg) => {
send_message(&SocketMessage::MoveContainerToMonitorNumber(arg.target))?;
}
@@ -2012,6 +2082,10 @@ fn main() -> Result<()> {
flags.push(format!("'--tcp-port={port}'"));
}
if arg.clean_state {
flags.push("'--clean-state'".to_string());
}
let script = if flags.is_empty() {
format!(
"Start-Process '{}' -WindowStyle hidden",
@@ -2026,7 +2100,7 @@ fn main() -> Result<()> {
};
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All);
system.refresh_processes(ProcessesToUpdate::All, true);
let mut attempts = 0;
let mut running = system
@@ -2047,7 +2121,7 @@ fn main() -> Result<()> {
print!("Waiting for komorebi.exe to start...");
std::thread::sleep(Duration::from_secs(3));
system.refresh_processes(ProcessesToUpdate::All);
system.refresh_processes(ProcessesToUpdate::All, true);
if system
.processes_by_name("komorebi.exe".as_ref())
@@ -2183,14 +2257,16 @@ if (!(Get-Process masir -ErrorAction SilentlyContinue))
}
println!("\nThank you for using komorebi!\n");
println!("# Sponsorship");
println!("# Commercial Use License");
println!("* View licensing options https://lgug2z.com/software/komorebi - A commercial use license is required to use komorebi at work");
println!("\n# Personal Use Sponsorship");
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - $5/month makes a big difference");
println!("* Leave a tip https://ko-fi.com/lgug2z - An alternative to GitHub Sponsors");
println!("\n# Community");
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
println!(
"* Subscribe to https://youtube.com/@LGUG2Z - Development videos, feature previews and release overviews"
);
println!("\n# Community");
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
println!("* Explore the Awesome Komorebi list https://github.com/LGUG2Z/awesome-komorebi - Projects in the komorebi ecosystem");
println!("\n# Documentation");
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
@@ -2220,6 +2296,30 @@ if (!(Get-Process masir -ErrorAction SilentlyContinue))
let stdout = String::from_utf8(output.stdout)?;
println!("{stdout}");
}
let client = reqwest::blocking::Client::new();
if let Ok(response) = client
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
.header("User-Agent", "komorebic-version-checker")
.send()
{
let version = env!("CARGO_PKG_VERSION");
#[derive(Deserialize)]
struct Release {
tag_name: String,
}
if let Ok(release) =
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
{
let trimmed = release.tag_name.trim_start_matches("v");
if trimmed > version {
println!("An updated version of komorebi is available! https://github.com/LGUG2Z/komorebi/releases/v{trimmed}");
}
}
}
}
SubCommand::Stop(arg) => {
if arg.whkd {
@@ -2293,9 +2393,13 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
}
}
send_message(&SocketMessage::Stop)?;
if arg.ignore_restore {
send_message(&SocketMessage::StopIgnoreRestore)?;
} else {
send_message(&SocketMessage::Stop)?;
}
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All);
system.refresh_processes(ProcessesToUpdate::All, true);
if system.processes_by_name("komorebi.exe".as_ref()).count() >= 1 {
println!("komorebi is still running, attempting to force-quit");
@@ -2443,6 +2547,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::ClearAllWorkspaceRules => {
send_message(&SocketMessage::ClearAllWorkspaceRules)?;
}
SubCommand::EnforceWorkspaceRules => {
send_message(&SocketMessage::EnforceWorkspaceRules)?;
}
SubCommand::Stack(arg) => {
send_message(&SocketMessage::StackWindow(arg.operation_direction))?;
}
@@ -2688,6 +2795,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::BorderImplementation(arg) => {
send_message(&SocketMessage::BorderImplementation(arg.style))?;
}
SubCommand::StackbarMode(arg) => {
send_message(&SocketMessage::StackbarMode(arg.mode))?;
}
SubCommand::Transparency(arg) => {
send_message(&SocketMessage::Transparency(arg.boolean_state.into()))?;
}

View File

@@ -77,6 +77,7 @@ nav:
- cli/quickstart.md
- cli/start.md
- cli/stop.md
- cli/kill.md
- cli/check.md
- cli/configuration.md
- cli/bar-configuration.md
@@ -103,9 +104,11 @@ nav:
- cli/force-focus.md
- cli/cycle-focus.md
- cli/cycle-move.md
- cli/eager-focus.md
- cli/stack.md
- cli/unstack.md
- cli/cycle-stack.md
- cli/cycle-stack-index.md
- cli/focus-stack-window.md
- cli/stack-all.md
- cli/unstack-all.md
@@ -129,6 +132,7 @@ nav:
- cli/focus-workspaces.md
- cli/focus-monitor-workspace.md
- cli/focus-named-workspace.md
- cli/close-workspace.md
- cli/cycle-monitor.md
- cli/cycle-workspace.md
- cli/move-workspace-to-monitor.md
@@ -196,6 +200,7 @@ nav:
- cli/clear-workspace-rules.md
- cli/clear-named-workspace-rules.md
- cli/clear-all-workspace-rules.md
- cli/enforce-workspace-rules.md
- cli/identify-object-name-change-application.md
- cli/identify-tray-application.md
- cli/identify-layered-application.md
@@ -207,6 +212,7 @@ nav:
- cli/border-offset.md
- cli/border-style.md
- cli/border-implementation.md
- cli/stackbar-mode.md
- cli/transparency.md
- cli/transparency-alpha.md
- cli/toggle-transparency.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.31`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.33`",
"type": "object",
"properties": {
"animation": {
@@ -13,13 +13,35 @@
"properties": {
"duration": {
"description": "Set the animation duration in ms (default: 250)",
"type": "integer",
"format": "uint64",
"minimum": 0.0
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
]
},
"enabled": {
"description": "Enable or disable animations (default: false)",
"type": "boolean"
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "boolean"
}
},
{
"type": "boolean"
}
]
},
"fps": {
"description": "Set the animation FPS (default: 60)",
@@ -29,38 +51,80 @@
},
"style": {
"description": "Set the animation style (default: Linear)",
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
]
}
},
{
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
]
}
]
}
}
@@ -420,7 +484,7 @@
"format": "int32"
},
"border_z_order": {
"description": "Active window border z-order (default: System)",
"description": "DEPRECATED from v0.1.31: no longer required",
"type": "string",
"enum": [
"Top",
@@ -579,7 +643,7 @@
}
},
"focus_follows_mouse": {
"description": "END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)",
"description": "END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead",
"oneOf": [
{
"description": "A custom FFM implementation (slightly more CPU-intensive)",
@@ -1163,6 +1227,15 @@
"RightMainVerticalStack"
]
},
"layout_flip": {
"description": "Specify an axis on which to flip the selected layout (default: None)",
"type": "string",
"enum": [
"Horizontal",
"Vertical",
"HorizontalAndVertical"
]
},
"layout_rules": {
"description": "Layout rules (default: None)",
"type": "object",
@@ -1384,6 +1457,89 @@
]
}
},
"remove_titlebar_applications": {
"description": "HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars",
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"type": "string",
"enum": [
"Exe",
"Class",
"Title",
"Path"
]
},
"matching_strategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}
},
{
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"type": "string",
"enum": [
"Exe",
"Class",
"Title",
"Path"
]
},
"matching_strategy": {
"type": "string",
"enum": [
"Legacy",
"Equals",
"StartsWith",
"EndsWith",
"Contains",
"Regex",
"DoesNotEndWith",
"DoesNotStartWith",
"DoesNotEqual",
"DoesNotContain"
]
}
}
}
}
]
}
},
"resize_delta": {
"description": "Delta to resize windows by (default 50)",
"type": "integer",

View File

@@ -7,5 +7,6 @@ with pkgs;
python311Packages.mkdocs-material
python311Packages.mkdocs-macros
python311Packages.setuptools
python311Packages.json-schema-for-humans
];
}