Compare commits

...

286 Commits

Author SHA1 Message Date
Rejdukien
10af1f3c84 fix(wm): clear last error before calling GetWindowLongPtrW
Apparently there is a quirk of GetWindowLongPtr when querying styles. If
the style bits are genuinely 0, the API returns 0 but does not clear the
last error.

If a previous API call set an error, GetWindowLongPtr might define
"failure" as "return 0 AND GetLastError != 0".

Hence, it is important to clear the last/previous error before calling
GetWindowLongPtrW to ensure that we don't misinterpret a valid 0 return
value as an error.
2026-01-08 18:51:06 -08:00
LGUG2Z
dcb1cd32fa chore(deps): cargo update 2026-01-08 18:25:33 -08:00
LGUG2Z
17a8ac5810 feat(config): add default_workspace_layout opt
This commit adds a default_workspace_layout opt, which defaults to BSP
to maintain backwards compatibility. This can also be set to "None".

When set to "None" or omitted, the default behaviour for new or
undefined workspaces (i.e. on monitors without config blocks) will be
non-tiling.  Otherwise, the given value will be the default layout
applied.
2026-01-08 18:12:30 -08:00
LGUG2Z
8805fafa99 feat(bar): make explicit monitor config optional
This commit makes the monitor configuration option for the komorebi-bar
optional, defaulting to index 0.
2026-01-04 19:15:24 -08:00
LGUG2Z
1219c3d118 refactor(bar): inline defaults in schemars attrs 2026-01-04 19:08:41 -08:00
LGUG2Z
b792328676 fix(splash): show mdm workspace tenant name instead of mdm url
The WorkspaceTenantName is populated far more consistently than MdmUrl.

This commit switches to extracting that instead and passing it on to the
MDM splash screen so that users on non-corporate devices who may have
unintentionally enrolled themselves into BYOD MDM by logging into an
account a clicking "Yes" on some dark pattern pop-up have a clear
indication of why they are seeing the splash, and can take the
appropriate steps to remove the MDM profile from their system if
desired.
2026-01-04 07:33:32 -08:00
LGUG2Z
a7380db47c docs(readme): add debugging instructions for people who don't know they've been mdm'd 2026-01-04 07:15:59 -08:00
LGUG2Z
086993a7c0 fix(cli): respect --bar flag in start even when no config is resolved
I think this got broken as part of the automatic Rust version syntax
upgrades which joined two if clauses together with && which should have
been kept separate.

Now, if the user gives the --bar flag to the start command, and a static
config file is not resolved, or if the static config file does not have
a bar_configurations stanza, it will fallthrough to the default
PowerShell snippet to try and start komorebi-bar without an explicit
--config flag.
2026-01-03 16:55:59 -08:00
LGUG2Z
51e1337f40 fix(wm): consider window count when calculating scrolling column width
This commit fixes the stupidest of stupid bugs. Column width
calculations on the Scrolling layout should take the number of windows
into account, especially when lower than the configured column count.
2026-01-01 13:07:18 -08:00
LGUG2Z
6cef8d9ef6 refactor(schema): simplify defaults in static_config 2025-12-31 13:04:34 -08:00
LGUG2Z
6e36b81669 build(nix): add flake.nix for cross-compilation from darwin and linux
I was getting really tired of having to switch between display inputs to
different platform-specific machines to be able to make and test changes
on komorebi for Windows and komorebi for Mac.

With this commit, the `flake.nix` provides a Nix devShell and crane
build for users to make and validate changes with `cargo check`, `cargo
clippy` and `cargo build` with the Windows MSVC toolchain on Linux and
macOS.
2025-12-28 17:24:41 -08:00
LGUG2Z
0758c7d900 refactor(theme): centralize theme-related code in komorebi-themes
Getting tired of making little changes in both this and the komorebi for
Mac repo - I think eventually either komorebi-themes will live in its
own repo or komorebi for Mac will be integrated here.

But for now, at least everything is defined in komorebi-themes and I
don't have to redefine any theme-related stuff in komorebi for Mac.
2025-12-27 21:58:39 -08:00
LGUG2Z
f77a303b30 docs(schema): ensure every enum variant has a renderable title 2025-12-27 20:34:31 -08:00
LGUG2Z
3a81f7babb docs(schema): ensure all public-facing bar config opts have docstrings 2025-12-27 13:40:45 -08:00
LGUG2Z
3d8778a7d6 docs(schema): ensure all public-facing static config opts have docstrings 2025-12-27 12:58:46 -08:00
LGUG2Z
a42e809ade docs(config): encode defaults of unwrapped options into schema
This commit ensures that the various default values that the different
Option<T> config properties can be unwrapped to are encoded by schemars
so that they can be picked up by docgen.
2025-12-26 18:16:55 -08:00
LGUG2Z
66c5766848 feat(cli): add custom output fir for docgen cmd 2025-12-26 10:26:00 -08:00
LGUG2Z
ac56590791 chore(clippy): address warnings 2025-12-23 17:19:00 -08:00
LGUG2Z
4d67f5fed3 build(just): update schemagen and add schemapub 2025-12-22 20:45:44 -08:00
LGUG2Z
c91224295f docs(schema): avoid duplicating display format enums in bar 2025-12-22 19:30:29 -08:00
LGUG2Z
4b1e3bd448 docs(schema): add deprecations and improve formatting 2025-12-21 21:56:17 -08:00
LGUG2Z
1cb8ed7f10 chore(deps): bump schemars to 1.1 2025-12-21 16:26:25 -08:00
LGUG2Z
90271f1f99 docs(schema): clarify that 'theme' take priority over 'border_colours' 2025-12-20 21:12:56 -08:00
LGUG2Z
9fde9ed6f8 chore(deps): cargo update 2025-12-20 11:40:31 -08:00
LGUG2Z
ce4479eb9f chore(deps): cargo update 2025-12-16 18:05:51 -08:00
LGUG2Z
f3b7f5ac42 chore(clippy): odd unused_assignment failures in rust 1.92.0 2025-12-14 20:00:18 -08:00
LGUG2Z
382896dfd2 docs(readme): add detailed instructions for students w/ mdm devices 2025-12-14 14:16:48 -08:00
LGUG2Z
ff4187aecf chore(dev): begin v0.1.40-dev 2025-12-14 14:07:44 -08:00
LGUG2Z
8f982e45d1 chore(release): v0.1.39 2025-12-09 17:06:14 -08:00
LGUG2Z
309bfdfee1 chore(deps): update dependencies.json 2025-12-09 16:55:47 -08:00
LGUG2Z
a0239faea0 refactor(cli): use "args" var name uniformly 2025-12-07 19:09:56 -08:00
LGUG2Z
999dd5d20d chore(deps): cargo update 2025-12-06 14:02:52 -08:00
bilogic
69086299e1 ci(github): render backticks in bug report template 2025-11-30 11:30:39 -08:00
LGUG2Z
4114b10b05 chore(deps): cargo update 2025-11-25 09:00:14 -08:00
LGUG2Z
71402fe01b docs(wm): update splash for students 2025-11-25 08:59:35 -08:00
dependabot[bot]
5c2767d589 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 14:42:05 -08:00
LGUG2Z
ac67c41377 docs(schema): update jsonschema 2025-11-23 10:56:01 -08:00
Csaba
faed6ec535 fix(bar): update the network widget in a separate thread
This commit changes the way how the default interface and network
activity is loaded by spawning a new thread on each request. This way
the main thread is not blocked by this process.

There has been instances where getting the default interface blocked the
egui ui thread, resulting in a frozen bar.

fix #1423 #1499
2025-11-23 10:55:55 -08:00
LGUG2Z
4ebba08b7d chore(deps): bump whkd to v0.2.10 2025-11-22 12:47:11 -08:00
alex-ds13
a733fecb49 fix(client): expose more useful structs 2025-11-22 12:44:48 -08:00
alex-ds13
31c969fc55 fix(wm): promoting a container shouldn't change resize dimensions
- When promoting a container it is using the
  `remove_focused_container()` and `insert_container_at_idx` functions
  which will remove the focused container size from `resize_dimensions`
  and then add the new container with size `None`. Then it was adding
  the previously stored size of the main container back to the first
  one. However this would result in at least 3 issues:

    1. We were removing the size from index 0, however when inserting it
       back on the primary_idx we might not add it to index 0, specially
       now that there could be locked containers so if container 0 is
       locked we would be adding it to the next container, which would
       result in the main container having no size information which
       would result in a recalculation on the next update and it would
       revert back to default, so if a user had resized their containers
       that information would be lost.

    2. We were removing the size from index 0. Then on
       `remove_focused_container` it would remove the size from the
       focused container index, but since we had already removed one
       size from the `resize_dimensions` that index would no longer
       match the actual focused container, so once again we would be
       losing information.

    3. Since we were removing the size of the focused container idx from
       `resize_dimensions` (albeit the wrong one as stated above), this
       would mean that the container that moved to that position would
       have no resize information so it would show up as if the main
       container was taking up half width of the area, ignoring any user
       resize, until the next workspace update was triggered.

- This commit completely ignores the `resize_dimensions` since promoting
  a container simply means moving one container from one position to the
  main one (usually index 0) and then shift all the rest to the "right".
  The existing `resize_dimensions` should be kept untouched. To do this
  we use the functions `remove_respecting_locks` and
  `insert_respecting_locks` on the containers themselves, which simply
  move the container and shifts the others which is precisely what we
  want.
2025-11-22 12:43:02 -08:00
LGUG2Z
df2adde13d feat(wm): remove scrolling layout column fallbacks
This commit removes the fallback to the columns layout when the number
of columns on the scrolling layout is less than 3.
2025-11-18 16:00:28 -08:00
LGUG2Z
a77b3e774a fix(docs): replace deprecated aliases in komorebi.bar.example.json
This commit ensures that we are using show_total_activity and
show_activity instead of show_total_data_transmitted and
show_network_activity in the komorebi.bar.example.json file which is
populated for users in the quickstart command.

JSONSchema is not smart enough to resolve aliases for backwards compat,
which results in confusing behaviour for new users trying to edit this
file.

fix #1596
2025-11-14 08:02:04 -08:00
LGUG2Z
c8c4c3507c chore(deps): handle egui-related upgrades 2025-11-12 16:20:19 -08:00
LGUG2Z
df38facf9e feat(wm): make promote-swap reversible
This commit makes the komorebic promote-swap command reversible by
storing the previous container index in the Workspace state.

If the current container index is the same as the layout's primary
index, and there is a previous promotion swap container index available,
the two containers at those indices will be swapped, effectively making
a second call to promote-swap an undo.
2025-11-04 13:18:35 -08:00
LGUG2Z
416dd94670 feat(wm): promote containers by swapping indices
This commit adds a new SocketMessage variant PromoteSwap and
corresponding komorebic promote-swap command.

PromoteSwap will also promote the focused window container to the
largest tile in the layout, but instead of removing the focused
container x from the Ring and inserting it into position 0, possibly
changing the positions of other windows between 0 and x, the indices x
and 0 in the Ring will be swapped directly.
2025-11-04 08:02:07 -08:00
LGUG2Z
d0ae92ca3a feat(wm): add visual feedback for preselection
This commit adds visual feedback in the form of a ghost tile for
preselections made by the preselect-direction command.

A container with the id "PRESELECT" will be added to the workspace, and
replaced when the next manage-able window is spawned.

A new command, cancel-preselect, has been added to remove both the
preselection index and the ghost tile if the user changes their mind.
2025-11-02 11:06:39 -08:00
LGUG2Z
3491dc7590 docs(readme): add note for people looking for komorebi for mac 2025-10-31 19:26:25 -07:00
LGUG2Z
adbb6c1cb0 feat(wm): add direction preselection
This commit adds a new feature to preselect the direction of the next
spawned window with a corresponding komorebic preselect-direction
command which takes an OperationDirection.

If the OperationDirection is valid from the current position, it will be
stored in the Workspace state, and then read, applied, and deleted when
the next manage-able window is spawned.

Direction preselection does not (yet?) support the Grid layout.
2025-10-31 15:08:57 -07:00
alex-ds13
18ee667896 fix(wm): simplify stack-all command
This commit makes the `stack-all` command simpler, by simply making a
`VecDeque` with all the tiled windows of the workspace, then it creates
a new container with that `VecDeque` and changes the workspace
containers to be just this new container which has all the windows in
it.

Then and only then do we focus and show the previously focused window
and hide all the rest.

This way we don't have a bunch of `FocusChange`/`Cloak`/`Uncloak` events
coming up which might conflict with each other when the user has
transparency and animations enabled.

With this commit, there will only be one focus event for the focused
window and one cloak event for each of the other windows.
2025-10-30 13:57:17 -07:00
LGUG2Z
1a42c64620 feat(cli): add feedback to the license cmd 2025-10-29 08:48:25 -07:00
LGUG2Z
b613986474 feat(wm): add a splash screen for mdm devices
A little nudge for people to do the right thing, 1990s vibes.
2025-10-28 18:03:17 -07:00
dependabot[bot]
e93751aa5f chore(deps): bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 09:07:45 -07:00
dependabot[bot]
9872dcd0d7 chore(deps): bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 09:07:32 -07:00
LGUG2Z
f99c82f5d4 ci(github): reduce workflow costs 2025-10-20 08:12:27 -07:00
LGUG2Z
b255058467 docs(cli): add eol annotations to ahk flags
Marking all --ahk flags in the start/stop/kill/enable-autostart commands
as EOL; there are any number of ways that users can manage their own
launching of AutoHotKey scripts and the complexity of the different ways
that AHK can be installed is not worth the maintenance burden for this
project.
2025-10-17 16:41:06 -07:00
LGUG2Z
2abe618354 feat(wm): add opt to constrain grid layout by rows
This commit adds a new LayoutOptions option, GridLayoutOptions,
currently with a single configurable "rows" opt which can be use to
constrain the grid by number of rows.
2025-10-17 16:41:03 -07:00
LGUG2Z
e953715fef feat(wm): track virtual desktop ids on state and global state
Also moved out State and GlobalState to state.rs to match the emerging
pattern in komorebi for Mac.
2025-10-14 13:11:12 -07:00
LGUG2Z
2ac1929117 feat(wm): add opt to keep focused column centered on scrolling layout
This commit adds a new option under layout_options.scrolling -
"center_focused_column", which defaults to false. When

set to true, and when the number of scrolling columns is an odd number
>=3, komorebi will, if there are enough windows being managed on the
workspace, and if the focused window is not too close to either the
beginning or the end of the workspace ring, keep the focused window in a
centered position in the layout
2025-10-10 17:39:32 -07:00
LGUG2Z
e33a5f28f0 chore(deps): bump windows-rs 2025-10-07 09:43:39 -07:00
LGUG2Z
15d069f2bf chore(deps): bump sysinfo and which 2025-10-07 09:11:20 -07:00
LGUG2Z
3e9947c2e2 ci(github): make sure clippy is installed 2025-10-01 16:04:28 -07:00
LGUG2Z
1c23439f95 chore(deps): bump egui and eframe to 0.32 2025-09-30 18:30:04 -07:00
LGUG2Z
6eb2905e00 refactor(rust): use eyre::Result uniformly
Matching the emerging style in komorebi for Mac
2025-09-30 18:21:14 -07:00
LGUG2Z
52a745d0a3 refactor(rust): upgrade to edition 2024 part 2 (clippy --fix) 2025-09-24 11:15:03 -07:00
LGUG2Z
80877cc449 refactor(rust): upgrade to edition 2024 part 1 2025-09-24 11:09:08 -07:00
LGUG2Z
160cb7202d chore(deps): cargo update 2025-09-20 16:59:07 -07:00
Csaba
8d085df1ba fix(bar): update network interface less frequently
This commit ensures that the default interface for the network widget does not update on every render of the bar.

Some people have experienced the bar being frozen and hopefully this will remediate that issue.
2025-09-20 15:59:59 -07:00
LGUG2Z
5d48a5c5b4 chore(clippy): apply latest fixes 2025-09-20 14:47:35 -07:00
LGUG2Z
539aeec965 refactor(rust): remove getset dependency 2025-09-20 14:34:19 -07:00
LGUG2Z
bcfb058bc3 refactor(rust): standardize on ok_or_eyre and bail!
This commit standardizes the codebase to disallow usage of the raw eyre!
macro for creating errors, instead using ok_or_eyre() when constructing
ad-hoc errors from Result and Option types, and otherwise using the
bail! macro in response to failed boolean conditions.
2025-09-20 12:59:09 -07:00
LGUG2Z
e07b464b0d chore(dev): begin 0.1.39-dev 2025-09-20 12:59:09 -07:00
LGUG2Z
ffa76ea28c chore(release): v0.1.38 2025-09-12 17:36:04 -07:00
Lucas de Linhares
2c00d79968 perf(cargo): add release-opt profile 2025-09-12 17:05:24 -07:00
LGUG2Z
78177af6b8 docs(mkdocs): run docgen, depgen and jsonschema targets 2025-09-09 08:56:29 -07:00
dependabot[bot]
86e0d40828 chore(deps): bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 10:03:38 -07:00
LGUG2Z
48f6ac8964 chore(deps): cargo update 2025-09-08 08:17:40 -07:00
omark96
1db572f789 feat(wm): implement Clone for State, Notification and NotificationEvent
This implements Clone for State, Notification and NotificationEvent to allow for the use of these
types in Rhai scripts.
2025-09-02 07:53:36 -07:00
1337cookie
3dad77533b feat(bar): add config opts for ro and removable disks
This lets the user enable or disable showing removable or read only
disks in the komorebi-bar storage widget.

Removable disks are set to show by default.

Read-only disks are set not to show by default. Having windows sandbox
installed displays a very long read only disk which could be problematic
for new users.
2025-09-01 15:15:41 -07:00
omark96
f40fb9a251 feat(wm): add config option to set tiling
This commit adds the option to set whether a workspace should be tiled or not by default. It retains
the default behaviour of komorebi, but adds the option to set a workspace to not be tiled by
default, but still be able to change the default layout for that workspace.
2025-09-01 15:04:59 -07:00
omark96
b4e16e43e9 feat(cli): change quickstart to prompt user before writing files
This commit makes it so the quickstart command first checks for the existence of the config files.
If they don't exist it writes them, if they do exist it prompts the user to whether or not they want
to overwrite the existing files. Lastly it prints the full path to the files that were written. This
is to prevent users from accidentally overwriting their configs as well as making it clearer where
komorebi places the config files for new users.
2025-09-01 15:03:48 -07:00
LGUG2Z
4c2e8ff6d2 chore(deps): cargo update 2025-09-01 15:03:09 -07:00
Jerry Kingsbury
c879aae1e7 test(wm): monocle on and off on nonexistent container
Created a test that tests moving a nonexistent container to monocle and
retreiving a nonexistent container from monocle. The test ensures we
receive an error when attempting to use monocle_on or monocle_off when a
container doesn't exist.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
7e87e83189 test(wm): toggle monocle on nonexistent container
Created a test to test toggle monocle when a container doesn't exist in
the monitor. The test ensures that we receive an error when attempting
to call toggle monocle when a monitor doesn't contain a container.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
aae9338f66 test(wm): toggle maximize a nonexistent window
The test ensures that we receive an error when attempting to toggle
maximize a window that doesn't exist.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
f68a709f1d test(wm): float nonexistent window
Created a test to test trying to float a nonexistent window. The test
ensures that we receive an error when trying to float a window in an
empty container
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
c76846ac63 test(wm): cycle windows in an empty container
Created a test to test cycling windows in an empty container. The test
ensures that when attempting to cycle windows in an empty container it
will push out an error.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
b29dd8b1d1 test(wm): remove nonexistent window from container
Created a test that tests removing a nonexistent window from a
container. The test creates a monitor containing an empty container and
attempts to remove a window from the container. The test ensures that
the result is an error when calling remove_window_from_container.
2025-08-27 18:05:07 -07:00
omark96
59c3c14731 feat(config): add work-area-offset per workspace
This commit adds the option to set 'work_area_offset' per workspace. If
no workspace work area offset is set for that workspace it will instead
use the value of the globals.work_area_offset for that workspace.

This commit adds a command to set the work area offset of a workspace
given a monitor index and a workspace index.
2025-08-27 18:05:07 -07:00
dependabot[bot]
db96f2cc5a chore(deps): bump reqwest from 0.12.22 to 0.12.23
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.22 to 0.12.23.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.22...v0.12.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-27 18:05:07 -07:00
dependabot[bot]
a37a6752a8 chore(deps): bump clap from 4.5.41 to 4.5.45
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.41 to 4.5.45.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.41...clap_complete-v4.5.45)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-27 18:05:07 -07:00
loeiks
4d0df9c5b5 fix(cli): fix typo in the toggle-pause docs 2025-08-27 18:05:07 -07:00
dependabot[bot]
f8ea62f857 chore(deps): bump shadow-rs from 1.2.0 to 1.2.1
Bumps [shadow-rs](https://github.com/baoyachi/shadow-rs) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/baoyachi/shadow-rs/releases)
- [Commits](https://github.com/baoyachi/shadow-rs/compare/v1.2.0...v1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:09:45 -07:00
dependabot[bot]
93bb41737b chore(deps): bump serde_json from 1.0.141 to 1.0.142
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.141 to 1.0.142.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.141...v1.0.142)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:09:35 -07:00
dependabot[bot]
280352eeef chore(deps): bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:06:45 -07:00
dependabot[bot]
7619b9b4ed chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:06:36 -07:00
dependabot[bot]
72a4d5276e chore(deps): bump slab from 0.4.10 to 0.4.11
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 07:07:54 -07:00
JustForFun88
4bb3b83d57 refactor(bar) split komorebi widget into smaller parts
This PR significantly refactors the komorebi  bar rendering logic,
simplifying state management, and addressing some found bugs. The
primary motivation was to make the codebase more readable and
maintainable.

Key Changes:

- Allocation Reduction: Removed most per-frame structure allocations.

- Runtime Matching Elimination: Replaced runtime pattern matching with
  pre-selected function pointers determined at initialization. Widget
  validations and configurations are now performed during widget
  creation rather than per-frame checks. For example, widget enablement
  is now handled by an Option that wraps each ..Bar structure. If a
  widget is enabled, its structure is present; otherwise, it is None.
  This eliminates the need for runtime enabled checks.

- Widget Modularity: Code is split into smaller parts, reducing
  complexity.

Bug Fixes:

- Corrected icon sizing for floating windows following regular
  containers, ensuring icons revert correctly from icon_size to
  text_size.

- There was also another bug with a floating window positioned above a
  monocle container, but I forgot the details 😅
2025-07-31 17:14:39 -07:00
omark96
ccd2f3a464 fix(wm): move last_focused_workspace logic to focus_workspace method
There are currently a number of commands that do not update the previously focused workspace
correctly. The previous method relied on handling each case, this change makes it so that
last_focused_workspace is always updated whenever the focus is changed.
2025-07-23 09:47:57 -07:00
dependabot[bot]
7e242ada66 chore(deps): bump netdev from 0.35.3 to 0.36.0
---
updated-dependencies:
- dependency-name: netdev
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 10:59:39 -07:00
dependabot[bot]
5b2acd0f12 chore(deps): bump shadow-rs from 1.1.1 to 1.2.0
Bumps [shadow-rs](https://github.com/baoyachi/shadow-rs) from 1.1.1 to 1.2.0.
- [Release notes](https://github.com/baoyachi/shadow-rs/releases)
- [Commits](https://github.com/baoyachi/shadow-rs/compare/v1.1.1...v1.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-20 19:14:59 -07:00
LGUG2Z
ec0bbaae98 chore(deps): cargo update 2025-07-20 18:49:37 -07:00
LGUG2Z
3c44f3bfdb refactor(clippy): apply lints 2025-07-19 16:42:25 -07:00
Martin Hjulstad Bednar
7839980ddf feat(shortcuts): show all hotkey bindings
- Removed command filtering logic from the shortcuts UI.
- All hotkey bindings are now shown regardless of command.
- Improved UI with hint text for the filter input.
2025-07-16 21:04:15 -07:00
JustForFun88
6416c0b6eb refactor(wm): move 'locked' flag down to containers
Key Changes
- Added `locked: bool` field directly to the `Container` struct.
- Removed `locked_containers` from `Workspace`.
- Updated `komorebi_bar` to access `locked` directly
  from `Container`.

Insert and swap operations respects `locked` container
indexes in the sequence
2025-06-15 12:30:03 -07:00
LGUG2Z
f6ccec9505 chore(deps): cargo update 2025-06-14 16:44:55 -07:00
LGUG2Z
21cb5e1e6f feat(stackbar): set title as default label
Following discussing in #1475, and considering the default of the
komorebi widget in komorebi-bar, this commit updates the default
StackbarLabel from Process to Title.

resolve #1475
2025-06-05 22:03:53 -07:00
LGUG2Z
009c0dcd28 chore(deps): cargo update 2025-06-04 15:54:15 -07:00
LGUG2Z
98c5ab3b9b fix(wm): prevent stack-all issues with n>1 stacks on ws
This commit ensures that unstack_all is called on a workspace before
attempting to proceed with stack_all to avoid stacking loops that can
occur when there are multiple pre-existing stacks already created on the
workspace.
2025-06-04 15:51:05 -07:00
dependabot[bot]
d4eeec994f chore(deps): bump netdev from 0.34.0 to 0.35.1
Bumps [netdev](https://github.com/shellrow/netdev) from 0.34.0 to 0.35.1.
- [Release notes](https://github.com/shellrow/netdev/releases)
- [Commits](https://github.com/shellrow/netdev/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 10:47:34 -07:00
dependabot[bot]
e9ed1cfd3b chore(deps): bump reqwest from 0.12.15 to 0.12.19
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.15 to 0.12.19.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.15...v0.12.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 10:47:05 -07:00
dependabot[bot]
4a2eb391f7 chore(deps): bump clap from 4.5.38 to 4.5.39
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.38 to 4.5.39.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.38...clap_complete-v4.5.39)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 10:46:48 -07:00
LGUG2Z
41e18bccc6 fix(stackbar): show regular cursor on hover
This commit ensures that the WM_SETCURSOR message is handled by stackbar
windows by setting the cursor to the default IDC_ARROW, which prevents
the spinning "Loading" icon from showing on hover.

re #1456
2025-05-27 08:12:58 -07:00
LGUG2Z
3d373b3630 fix(schema): flatten bar mouse config opts 2025-05-26 09:52:14 -07:00
LGUG2Z
b4e61b079c feat(wm): add scrolling layout
This commit adds a new DefaultLayout::Scrolling variant, along with a
new LayoutOptions configuration which will initially be used to allow
the user to declaratively specify the number of visible columns for the
Scrolling layout, and a new komorebic "scrolling-layout-columns" command
to allow the user to modify this value for the focused workspace at
runtime.

The Scrolling layout is inspired by the Niri scrolling window manager,
presenting a workspace as an infinite scrollable horizontal strip with a
viewport which includes the focused window + N other windows in columns.
There is no support for splitting columns into multiple rows.

This layout can currently only be applied to single-monitor setups as
the scrolling would result in layout calculations which push the windows
in the columns moving out of the viewport onto adjacent monitors.

This implementation in the current state is enough to be useable for me
personally, but if others want to iterate on this, make it handle
hiding/restoring windows correctly when scrolling the viewport so that
adjacent monitors don't get impacted etc., patches are always welcome.

resolve #1434
2025-05-17 21:12:51 -07:00
LGUG2Z
eec6312a51 chore(dev): begin v0.1.38-dev 2025-05-17 20:26:13 -07:00
LGUG2Z
00384ce333 chore(release): v0.1.37 2025-05-17 11:44:55 -07:00
LGUG2Z
e75578d9ce ci(github): skip backwards compat test 2025-05-17 08:25:23 -07:00
alex-ds13
50c850cb02 feat(wm): floating over monocle
This commit allows for floating windows on the floating workspace layer
to be toggled on and off when monocle mode is enabled on a workspace.
2025-05-16 16:35:47 -07:00
Jerry Kingsbury
5a1af5b133 test(wm): swap container with non-existent container
Created a test for swapping a container with a container that doesn't
exist, which ensures that we receive an error and that the contents of
the container is still available when attempting to swap a container
with a non-existent one.

A bug was discovered where the origin container was being removed even
though the swap_container function would return an error.

@LGUGZ discovered that the issue was due to the origin container being
removed before verifying that both containers exist. The fix that @LGUGZ
came up with is included.
2025-05-16 16:24:02 -07:00
Jerry Kingsbury
74c433185a test(wm): switch focus to non-existent monitor
Created a test for focusing on a monitor that doesn't exist, which
ensures that we receive an error and that we are still focused on the
current monitor when attempting to focus a non-existent monitor.
2025-05-16 16:24:02 -07:00
Jerry Kingsbury
71bb346c89 test(wm): swap workspace with non-existent monitor
Created a test for swapping workspaces with a monitor that doesn't
exist, which ensures that we receive an error and that none of the
workspaces were moved when attempting to swap workspaces with a monitor
that doesn't exist.
2025-05-16 16:23:55 -07:00
LGUG2Z
3feff1dca9 docs(schema): update jsonschema and docgen 2025-05-15 18:28:27 -07:00
JustForFun88
3019eaf89c refactor(bar): app widget and icon caching
PR #1439 authored and submitted by @JustForFun88

I understand this PR combines two areas of work — refactoring the
Applications widget and introducing a new icon caching system —
which would ideally be submitted separately.

Originally, I only intended to reduce allocations and simplify icon
loading in `applications.rs`, but as I worked through it, it became
clear that a more general-purpose caching system was needed. One
improvement led to another ... 😄

Apologies for bundling these changes together. If needed, I’m happy to
split this PR into smaller, focused ones.

Key Changes

- Introduced `IconsCache` with unified in-memory image & texture
  management.
- Added `ImageIcon` and `ImageIconId` (based on path or HWND) for
  caching and reuse.
- `Icon::Image` now wraps `ImageIcon`, decoupled from direct `RgbaImage`
  usage.
- Extracted app launch logic into `UserCommand` with built-in cooldown.
- Simplified config parsing and UI hover rendering in `App`.
- Replaced legacy `ICON_CACHE` in
  `KomorebiNotificationStateContainerInformation`
  → Now uses the shared `ImageIcon::try_load(hwnd, ..)` with caching and fallback.

Motivation

- Reduce redundant image copies and avoid repeated pixel-to-texture
  conversions.
- Cleanly separate concerns for launching and icon handling.
- Reuse icons across `Applications`, Komorebi windows, and potentially
  more in the future.

Tested

- Works on Windows 11.
- Verified path/exe/HWND icon loading and fallback.
2025-05-15 18:17:27 -07:00
LGUG2Z
ce59bd9ae4 chore(deps): cargo update 2025-05-15 17:59:57 -07:00
LGUG2Z
309dd159ca docs(mkdocs): updates to prepare for v0.1.37 2025-05-15 17:45:16 -07:00
LGUG2Z
6f1d6dbdc7 fix(wm): disallow toggle-float if ws has a monocle container
This commit ensures that the user can't get into a weird state by
attempting to float a monocle container. If a user attempts to call
toggle-float on a monocle container, a warning will be logged and the
command will be ignored.
2025-05-15 08:09:10 -07:00
LGUG2Z
6a10d583a6 build(cargo): propagate expensive schemars feature correctly 2025-05-11 20:00:15 -07:00
LGUG2Z
270ea5aa46 feat(cli): add focused-container-kind state query
This commit adds a new StateQuery variant, FocusedContainerKind, which
will will return None if there is not focused container on the current
workspace (ie. it is empty), Single if the focused container contains a
single window, or Stack if the focused container contains more than one
window.
2025-05-11 18:23:53 -07:00
dependabot[bot]
a9d2738733 chore(deps): bump chrono from 0.4.40 to 0.4.41
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.40 to 0.4.41.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.40...v0.4.41)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-10 15:22:32 -07:00
Csaba
3d8f68e559 feat(bar): send commands by mouse/touchpad/screen
This commit makes it possible to send commands from the bar by using the
mouse/touchpad/touchscreen.

Komorebi or custom commands can be sent by clicking on the mouse's
primary, secondary, middle, back or forward buttons.

As the primary single click is already used by widgets, only primary
double clicks can send commands. This limitation is due to Egui also
triggering 2 single clicks before a double click is triggered. Egui does
not have an implementation for stopping event propagation out of the box
and would be too much work to include.

Similarly, commands can be sent on every "tick" of mouse scrolling,
touchpad or touchscreen swiping in any of the 4 directions. This "tick"
can be adjusted to fit user's preference.

This is due to the fact, that Egui does not have an event for when a
mouse "tick" occurs. It instead gives a number of points that the user
scrolled/swiped on each frame.

PR: #1403
2025-05-10 15:19:24 -07:00
Jerry Kingsbury
80bb7288c4 test(wm): transfer window to nonexistent monitor
Created a test for the transfer_window function.

The tests attempts to transfer a window to a monitor that doesn't exist,
and checks to see if we return an error. The test successfully gets an
error but there is a bug where the window isn't in the contiainer after
a failed transfer. I wrote a note comment to explain the bug just in
case we need to reference back to it.
2025-05-10 15:01:17 -07:00
Jerry Kingsbury
76c833f661 test(wm): move workspace to non existent monitor
Created a test for moving the focused workspace to a monitor that hasn't
been created. The test ensures that we receive an error when moving a
workspace to a monitor that doesn't exist.
2025-05-10 15:01:17 -07:00
Jerry Kingsbury
80bce4be7e test(workspace): move window to non existent container
Created a test for moving a window to a container that doesn't exist.
The test ensures that we receive an error when moving a window to a
container that doesn't exist.
2025-05-10 15:01:17 -07:00
Jerry Kingsbury
8c10547325 test(workspace): remove a non existent window
Created a test for removing a window that doesn't exist.

The test creates a container with a window and then attempts to remove a
window that doesn't exist. The test will check the result to ensure that
the result returned an error and that we still have the expected number
of windows.
2025-05-10 15:01:10 -07:00
Jerry Kingsbury
8f886b3fe4 test(monitor): move container to a nonexistent workspace
Created a test for moving a container to a workspace that doesn't exist.

The test ensures when a container is moved to a workspace that doesn't
exist, the workspace is created and that the workspace contains the
container.

It also checks that there are N workspaces available where N is the
largest workspace number.
2025-05-10 15:01:06 -07:00
Jerry Kingsbury
53c38e157f test(monitor): remove nonexistent workspace
Created a test for moving a workspace that doesn't exist. The test
attempts to remove a workspace that doesn't exist and checks to ensure
the removed_workspace variable is None.
2025-05-10 14:58:17 -07:00
LGUG2Z
70f561e6ac feat(shortcuts): add helper written in egui
This commit adds a simple egui helper application which shows a list of
shortcuts defined in a user's whkdrc file. Parsing AHK files is not
supported.

In addition to listing out shortcuts defined in the whkdrc file, the top
line allows users to add filter a filter to narrow down the list of
commands and key bindings to the ones they are interested in.

A new komorebic command "toggle-shortcuts" has been introduced which
will first attempt to kill "komorebi-shortcuts.exe", and then exit if
the kill signal was successful (ie. a process was closed), or proceed to
open "komorebi-shortcuts.exe" if the kill signal was not successful (ie.
no process was closed, so we should open one).

"komorebi-shortcuts.exe" has been added as a floating application in
lib.rs to allow for users to use the "komorebic move" command to
manipulate its position via their existing keyboard bindings.
2025-05-07 08:52:08 -07:00
LGUG2Z
4ea835fa59 feat(wm): toggle monocle off on monitor container moves
This commit ensures that if a user sends one of the various messages
which ultimately call move_container_to_monitor, if the target workspace
on the target monitor currently has a monocle container, it will be
toggled off to allow space for the container being moved to be rendered.
2025-05-07 08:19:22 -07:00
LGUG2Z
09137af305 feat(wm): toggle monocle off on ws container moves
This commit ensures that if a user sends one of the various messages
which ultimately call move_container_to_workspace, if the target
workspace currently has a monocle container, it will be toggled off to
allow space for the container being moved to be rendered.
2025-05-06 17:03:57 -07:00
LGUG2Z
ee89b344df chore(deps): update dependencies.json 2025-05-04 11:38:03 -07:00
LGUG2Z
46d5ea4a1d refactor(wm): log errors when allow_set_foreground_window fails
This startup Win32 API call can sporadically fail, so this commit adds
some retry logic and logging of errors every time it fails. If it
crosses the retry threshold, the application will exit (because you
can't really have a tiling window manager running that doesn't let you
set the foreground window).
2025-05-02 17:49:29 -07:00
LGUG2Z
82c2241601 fix(wm): don't reap ws windows when using hide instead of cloak
This commit ensures that workspace windows that are hidden when the user
is using HidingBehaviour::Hide will not be unintentionally reaped by a
hard-coded workaround for Microsoft Office's continuing
enshittification.

HidingBehaviour::Hide has also been marked in the docs as an EOL
feature, and some dead code that was ultimately migrated to reaper.rs
has been cleaned up.

fix #1426
2025-05-02 17:29:41 -07:00
dependabot[bot]
c28773b96a chore(deps): bump miette from 7.5.0 to 7.6.0
Bumps [miette](https://github.com/zkat/miette) from 7.5.0 to 7.6.0.
- [Release notes](https://github.com/zkat/miette/releases)
- [Changelog](https://github.com/zkat/miette/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zkat/miette/commits/miette-derive-v7.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-29 08:27:05 -07:00
LGUG2Z
577364a556 feat(bar): hide when leaving komorebi's virtual desktop
This commit adds a new VirtualDesktopNotification which is used to
notify subscribers when the user leaves and enters the virtual desktop
associated with komorebi.

komorebi-bar consumes these notifications to minmize and restore the bar
appropriately depending on the currently focused virtual desktop.

re #1420
2025-04-28 14:33:39 -07:00
LGUG2Z
17cd0308cb feat(bar): improve path handling on apps widget
This commit improves path handling for commands and icons in the new
Application widget by making use of PathExt::replace_env when loading
the user-specified ApplicationsConfig.

Crucially for scoop users, this means that user-agnostic references to
scoop apps can now be made like this:

```
$Env:USERPROFILE/scoop/apps/zed-nightly/current/zed.exe
```

When attempting to look up an icon for a command, we now split the
command on ".exe", and if this is a complete path to a file, we try to
use it to extract an icon, otherwise we try to resolve a complete path
using "which" before doing the same.
2025-04-27 11:47:19 -07:00
Alisher Galiev
10424b696f feat(bar): add applications widget
This pull request introduces a new Applications widget that displays a
user-defined list of application launchers in the UI. Each app entry
supports an icon, a label, and executes its configured command on click.

The design of this widget is inspired by the Applications Widget of YASB
Reborn. I personally missed this functionality and aimed to bring a
similar experience to komorebi-bar.

Further information is in the text of PR #1415
2025-04-27 11:45:06 -07:00
LGUG2Z
6e7d8fb922 fix(animation): avoid redundant async window pos calls
As pointed out by @alex-ds13, animations run on a separate thread and so
unresponsive apps will not block komorebi.
2025-04-25 17:53:57 -07:00
LGUG2Z
917cd9b7db fix(borders): destroy all if a different vd is detected
This commit ensures that all borders will destroyed if komorebi detects
that the user has switched to a Windows Virtual Desktop different from
the one it was launched on and associated with.

There is still some work to be done to forcibly redraw the borders when
a navigation back to the VD associated with komorebi is detected, but
that requires tracking the state of the current VD ID outside of the
process_event handler somewhere.

re #1420
2025-04-24 21:41:21 -07:00
LGUG2Z
bdbd665b21 refactor(wm): add window handling sync/async enum
This commit adds a dedicated WindowHandlingBehaviour enum with Sync and
Async variants, and reverts a change to the render fn in window.rs to
use move_window instead of position_window if the
WindowHandlingBehaviour variant is Sync.
2025-04-24 18:50:54 -07:00
Kuukunen
f3f2098451 feat(wm): add configuration option for async window handling
Add configuration option for enabling/disabling the asynchronous window
handling. The feature might cause unforeseen issues, so by default it is
off. It can be enabled with setting the option async_window_handling to
true.
2025-04-24 18:50:21 -07:00
Kuukunen
4ca2e8388b fix(wm): fix unresponsiveness by using asynchronous window handling
Unresponsive windows might cause Komorebi to hang because functions like
SetWindowPos wait for the target window's WindowProc.

I changed most SetWindowPos calls to use the SWP_ASYNCWINDOWPOS flag,
which should avoid hanging, and I changed the one
WindowsApi::move_window call to use position_window instead. I also
changed the ShowWindow to ShowWindowAsync.

The only issue I noticed was that it caused the stackbar to disappear
(and/or flicker), probably because the window is modified right after
moving it, so I disabled the async position_window for that.
2025-04-24 18:50:21 -07:00
thearturca
31752e422a feat(animation): cubic-bezier for styles
This commit adds ability to use cubic-bezier as an animation style,
which allows users to customize the smoothness of animations.
2025-04-24 17:42:04 -07:00
Jerry Kingsbury
5e308b9131 test(wm): add window handle to move based on workspace rules test
Created a simple test for the
add_window_handle_to_move_based_on_workspace_rules function.

The test creates mock data representing window and the movement details.

The test will call the function with an empty vector to hold the
workspace rules and then check that the workspace rules are in the
vector.
2025-04-22 18:03:42 -07:00
Jerry Kingsbury
1bf53b89af test(wm): ensure named workspace for monitor test
Created a test for the ensure_named_workspace_for_monitor function.

The test creates two monitors and holds a list of workspace names.

When calling the ensure_named_workspace_for_monitor_function the test
checks to ensure that the the the monitor contains the length of the
list workspaces and that the workspaces uses the name in the list.

The test adds more names to the list and repeats the check on the other
monitor.
2025-04-22 18:03:13 -07:00
Jerry Kingsbury
11690c6004 test(wm): test toggle_monocle and toggle_maximize
Created tests for the toggle_monocle and toggle_maximize functions. The
test are simialar to the maximize and unmaximize test and the monocle on
and off test.
2025-04-22 18:02:59 -07:00
Jerry Kingsbury
3457dfc04c test(wm): monocle on and off test
Created a test for the monocle_on and monocle_off functions.

The test checks to ensure that the focused workspace container becomes a
monocle container when calling the monocle_on function.

The test will also ensure that the container is moved to the workspace's
container ring when the monocle_off function is called.
2025-04-22 18:02:23 -07:00
Jerry Kingsbury
af1c9b5aa9 test(wm): test maximize and unmaximize window
Created a test for maximizing and unmaximizing a window.

The test ensures that the when calling maximize_window, the focused
window is added to the maximized_window list.

When calling unmaximized window, the checks to ensure that None is
returned to ensure that the maximized window is removed from the list.

The test switches to a different window and performs the same checks.
2025-04-22 18:01:55 -07:00
dependabot[bot]
22fac5a9fb chore(deps): bump netdev from 0.33.0 to 0.34.0
Bumps [netdev](https://github.com/shellrow/netdev) from 0.33.0 to 0.34.0.
- [Release notes](https://github.com/shellrow/netdev/releases)
- [Commits](https://github.com/shellrow/netdev/compare/v0.33.0...v0.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-21 17:16:48 -07:00
LGUG2Z
7a3990f106 chore(deps): cargo update 2025-04-20 10:39:28 -07:00
alex-ds13
74e93e5524 fix(wm): ensure default CrossBoundaryBehaviour consistency
This commit fixes the default for `CrossBoundaryBehaviour` which was set
to `Monitor` when loading a user configuration but was set to
`Workspace` if starting from fresh without a user config.

The docs also stated that this value was `Monitor` by default.
2025-04-20 10:26:01 -07:00
alex-ds13
86e78570d6 fix(wm): reset global configs to default on removal 2025-04-20 10:26:01 -07:00
alex-ds13
3ee3aac806 feat(wm): float placement configs
This commit adds a few more options to combine with the
`FloatingLayerBehaviour` which determine placement in different
situations:

- `"toggle_float_placement"`: the placement to be used by a floating
  window when it is forced to float with the `toggle-float` command.
- `"floating_layer_placement"`: the placement to be used by a floating
  window when it is spawned on the floating layer and the user has the
  floating layer behaviour set to float.
- `"floating_override_placement"`: the placement to be used by a window
  that is spawned when float override is active.
- `"float_rule_placement"`: the placement to be used by a window that
  matches a 'floating_applications' rule.

Each `Placement` can be one of the following types:

- "None": windows are spawned wherever Windows positions them with
  whatever size they had. Komorebi does not change its size or position.
- "Center": windows are centered without changing their size.
- "CenterAndResize": windows are centered and resized according to the
defined aspect ratio.

By default the placements are as follows:
- `"toggle_float_placement"`: `"CenterAndResize"`
- `"floating_layer_placement"`: `"Center"`
- `"floating_override_placement"`: `"None"`
- `"float_rule_placement"`: `"None"`

This commit also adds the `floating_layer_behaviour` as a global config.
2025-04-20 10:25:57 -07:00
alex-ds13
c408c1149c fix(wm): fix eager focus on hidden stacked window
When using the eager focus command to focus a window that was hidden on
a stack on an unfocused workspace it would load the workspace focus the
correct window but keep it cloaked with the previously focused window
showing. This would happen because we were focusing the workspace before
focusing the window which would create two window focus events, the
first one for the previously focused window and the second one for the
target window we wanted. The problem was that when the first event was
handled it would again refocus that window and cloak the target window
again so when the target window focus event came it would ignore it
since the `should_manage` function would return `false` because the
window was cloaked and had all its window styles as `None`, unless we
had transparency enabled, in that case it would override it and create a
focus loop.

This commit fixes this by not loading the newly focused workspace
immediately. Instead we first focus the workspace if it is needed
without loading it and mark it as needing to be loaded. Then we focus
the correct target window and only then do we load the workspace. This
way the workspace loads with the correct window already focused.

This is the same approach that is currently being done on the
`perform_reconciliation` function when there is an alt-tab.
2025-04-17 07:57:45 -07:00
LGUG2Z
a8b02f40fd chore(dev): begin 0.1.37-dev 2025-04-13 09:38:12 -07:00
LGUG2Z
6608e5a5bb docs(schema): update schema.json 2025-04-13 09:37:13 -07:00
LGUG2Z
8ef1bcf26e chore(release): v0.1.36 2025-04-12 10:52:31 -07:00
LGUG2Z
d146f35c25 chore(deps): cargo update 2025-04-12 10:52:31 -07:00
dependabot[bot]
fc07ba3dd9 chore(deps): bump crossbeam-channel from 0.5.14 to 0.5.15
Bumps [crossbeam-channel](https://github.com/crossbeam-rs/crossbeam) from 0.5.14 to 0.5.15.
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-channel-0.5.14...crossbeam-channel-0.5.15)

---
updated-dependencies:
- dependency-name: crossbeam-channel
  dependency-version: 0.5.15
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 09:54:12 -07:00
LGUG2Z
3a8a61119d fix(config): update monitor wallpaper on reload 2025-04-09 16:09:17 -07:00
LGUG2Z
d24beb60b1 feat(config): add per-monitor floating layer behaviour opt
This commit adds an option to set the floating layer behaviour for all
workspaces on a monitor.

Overrides set on an individual workspace level will take precedence over
the option set at the monitor level.
2025-04-09 09:42:46 -07:00
Jerry Kingsbury
7daf3242e2 test(wm): add float and lock toggle tests
Created a test for the toggle_lock function. The test creates a
workspace with multiple containers and checks to see whether the
container was added or removed from the locked containers list when
calling the toggle_lock function.

Created a test for the float_window function. The test checks to ensure
that when calling the float_window function, the window is added to the
floating_windows list and removed from the containers window list. The
test will also check the count and ensure the expected window was added.
2025-04-09 08:55:10 -07:00
LGUG2Z
b6e261aef6 fix(bar): avoid retile messages on ws switch
This commit removes RetileWithResizeDimensions messages from the batches
that are sent to komorebi when changing workspace.

I'm not really sure why this was added in the first place, but removing
it doesn't seem to impact layouts on workspace switch, and more
important, by removing this message I'm no longer able to reproduce the
sudden exits of komorebi.exe under sustained workspace switching calls
made via the bar.
2025-04-08 17:20:02 -07:00
LGUG2Z
d40c304324 fix(borders): avoid deadlock on state hashmaps
This commit addresses a deadlock on WINDOW_BORDERS which can occur under
load, particularly when switching workspaces.

When switching workspaces using the komorebi-bar,
RetileWithResizeDimensions is also called, which calls
border_manager::destroy_all_borders.

At the same time, border_manager::window_border is called when hiding
borders from the previous workspace. Both of these functions try to take
locks on WINDOWS_BORDERS and BORDER_STATE.

border_manager::window_border is called in a new thread by hide_border,
so if this has not finished with the lock by the time
destroy_all_borders requests it, we get stuck in a deadlock.

The changes in this commit ensure that the BORDER_STATE lock is dropped
as early as possible in destroy_all_borders, and that we are not holding
the WINDOWS_BORDERS lock in window_border while trying to get the
BORDER_STATE lock to look up BorderInfo.

The logs which show the initial deadlock being detected:

2025-04-08T22:49:37.641888Z  WARN komorebi::process_command: could not acquire window manager lock, not processing message: FocusWorkspaceNumber
2025-04-08T22:49:38.294952Z  WARN komorebi::process_command: could not acquire window manager lock, not processing message: FocusWorkspaceNumber
2025-04-08T22:49:39.225645Z ERROR komorebi: 1 deadlocks detected
2025-04-08T22:49:39.225826Z ERROR komorebi: deadlock #0
2025-04-08T22:49:39.225950Z ERROR komorebi: thread id: 9896
2025-04-08T22:49:39.226072Z ERROR komorebi:    0:     0x7ff68fd33dec - backtrace::backtrace::dbghelp64::trace
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\dbghelp64.rs:99
                           backtrace::backtrace::trace_unsynchronized<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:66
   1:     0x7ff68fd33c87 - backtrace::backtrace::trace<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:53
   2:     0x7ff68fd3c03e - backtrace::capture::Backtrace::create
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:193
   3:     0x7ff68fd3bfae - backtrace::capture::Backtrace::new
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:158
   4:     0x7ff68f937b54 - parking_lot_core::parking_lot::deadlock_impl::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1211
   5:     0x7ff68f921f0e - parking_lot_core::parking_lot::deadlock::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1144
   6:     0x7ff68f92bf88 - parking_lot_core::parking_lot::park::closure$0<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:637
   7:     0x7ff68f92a777 - parking_lot_core::parking_lot::with_thread_data
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:207
                           parking_lot_core::parking_lot::park<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:600
   8:     0x7ff68f926fa0 - parking_lot::raw_mutex::RawMutex::lock_slow
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:262
   9:     0x7ff68f1daac6 - parking_lot::raw_mutex::impl$0::lock
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:72
  10:     0x7ff68ef05eb3 - lock_api::mutex::Mutex<parking_lot::raw_mutex::RawMutex,std::collections::hash::map::HashMap<isize,alloc::string::String,std::hash::random::RandomState> >::lock<parking_lot::raw_mutex::RawMutex,std::collections::hash::map::HashMap<isize,alloc::string::Str
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\lock_api-0.4.12\src\mutex.rs:223
  11:     0x7ff68ef0713d - komorebi::border_manager::destroy_all_borders
                               at komorebi\src\border_manager\mod.rs:139
  12:     0x7ff68f194e61 - komorebi::window_manager::WindowManager::process_command<ref_mut$<uds_windows::stdnet::net::UnixStream> >
                               at komorebi\src\process_command.rs:918
  13:     0x7ff68ef1fdcf - komorebi::process_command::read_commands_uds
                               at komorebi\src\process_command.rs:2292
  14:     0x7ff68f231eb5 - komorebi::process_command::listen_for_commands::closure$0::closure$0::closure$0
                               at komorebi\src\process_command.rs:123
  15:     0x7ff68f32bcf3 - core::hint::black_box
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\hint.rs:477
                           std::sys::backtrace::__rust_begin_short_backtrace<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\sys\backtrace.rs:152
  16:     0x7ff68f00d1b4 - std::thread::impl$0::spawn_unchecked_::closure$1::closure$0<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\thread\mod.rs:559
  17:     0x7ff68f356ea1 - core::panic::unwind_safe::impl$25::call_once<tuple$<>,std::thread::impl$0::spawn_unchecked_::closure$1::closure_env$0<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> > >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\panic\unwind_safe.rs:272
  18:     0x7ff68f1d1e00 - std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<std::thread::impl$0::spawn_unchecked_::closure$1::closure_env$0<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> > >,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:587
  19:     0x7ff68f0102b3 - std::thread::impl$7::drop::closure$0<enum2$<core::result::Result<tuple$<>,eyre::Report> > >
  20:     0x7ff68f007be7 - std::panicking::try
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:550
                           std::panic::catch_unwind
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panic.rs:358
                           std::thread::impl$0::spawn_unchecked_::closure$1<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\thread\mod.rs:557
  21:     0x7ff68ecfeb8e - core::ops::function::FnOnce::call_once<std::thread::impl$0::spawn_unchecked_::closure_env$1<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\ops\function.rs:250
  22:     0x7ff68fe84f1d - alloc::boxed::impl$28::call_once
                               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library\alloc\src\boxed.rs:1976
                           alloc::boxed::impl$28::call_once
                               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library\alloc\src\boxed.rs:1976
                           std::sys::pal::windows::thread::impl$0::new::thread_start
                               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library\std\src\sys\pal\windows\thread.rs:56
  23:     0x7ff8ef27257d - BaseThreadInitThunk
  24:     0x7ff8f042af28 - RtlUserThreadStart

2025-04-08T22:49:39.228617Z ERROR komorebi: thread id: 62364
2025-04-08T22:49:39.228731Z ERROR komorebi:    0:     0x7ff68fd33dec - backtrace::backtrace::dbghelp64::trace
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\dbghelp64.rs:99
                           backtrace::backtrace::trace_unsynchronized<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:66
   1:     0x7ff68fd33c87 - backtrace::backtrace::trace<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:53
   2:     0x7ff68fd3c03e - backtrace::capture::Backtrace::create
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:193
   3:     0x7ff68fd3bfae - backtrace::capture::Backtrace::new
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:158
   4:     0x7ff68f937b54 - parking_lot_core::parking_lot::deadlock_impl::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1211
   5:     0x7ff68f921f0e - parking_lot_core::parking_lot::deadlock::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1144
   6:     0x7ff68f92bf88 - parking_lot_core::parking_lot::park::closure$0<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:637
   7:     0x7ff68f92a777 - parking_lot_core::parking_lot::with_thread_data
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:207
                           parking_lot_core::parking_lot::park<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:600
   8:     0x7ff68f926fa0 - parking_lot::raw_mutex::RawMutex::lock_slow
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:262
   9:     0x7ff68f1daac6 - parking_lot::raw_mutex::impl$0::lock
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:72
  10:     0x7ff68ef05d93 - lock_api::mutex::Mutex<parking_lot::raw_mutex::RawMutex,std::collections::hash::map::HashMap<alloc::string::String,alloc::boxed::Box<komorebi::border_manager::border::Border,alloc::alloc::Global>,std::hash::random::RandomState> >::lock<parking_lot::raw_mu
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\lock_api-0.4.12\src\mutex.rs:223
  11:     0x7ff68f2064de - komorebi::border_manager::window_border::closure$0
                               at komorebi\src\border_manager\mod.rs:109
  12:     0x7ff68eddeca9 - enum2$<core::option::Option<ref$<alloc::string::String> > >::and_then<ref$<alloc::string::String>,komorebi::border_manager::BorderInfo,komorebi::border_manager::window_border::closure_env$0>
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\option.rs:1452
  13:     0x7ff68ef06339 - komorebi::border_manager::window_border
                               at komorebi\src\border_manager\mod.rs:108
2025-04-08 16:14:41 -07:00
LGUG2Z
69d252ba12 feat(wm): drop empty containers on ws update
We shouldn't ever have empty containers, but never say never because
someone on the Discord has an empty container with no Windows that
continues to take up a tile. This commit adds a call to drop all
containers without any windows whenever Workspace::update is called.
2025-04-07 15:28:29 -07:00
LGUG2Z
2ac477d89f docs(mkdocs): reduce number of tabs to improve nav 2025-04-07 11:02:57 -07:00
alex-ds13
6db73151f7 fix(wm): remove border width/offset when disabled
This commit makes sure the border's width and offset is removed when you
disable the borders.

If you want to still have that size on your gaps when borders are
disabled then you should add it to the `default_container_padding` or to
the per-monitor or per-workspace `container_padding`.
2025-04-07 10:12:18 -07:00
alex-ds13
2d6ff0708f fix(docs): borders are enabled by default 2025-04-07 10:12:18 -07:00
Csaba
13a519fb29 fix(cli): restart bar on replace-configuration
This commit ensures that the replace-configuration command also replaces
bars.

Already running bars are stopped and new bars are started using the new
configuration.
2025-04-06 21:19:41 -07:00
amrbashir
9f8e4b9dca feat(core): use PathExt to unify env var resolution
This new implementation allows for expanding any environment variable so
it is not limited to just `~`, `$HOME`, `$Env:USERPROFILE` and
`$Env:KOMOREBI_CONFIG_HOME`.

It expands the follwing formats:
- CMD: `%variable%`
- PowerShell: `$Env:variable`
- Bash: `$variable`

I searched throughout the code base for path and migrate any code that
might need to PathExt::replace_env.

It is possible that I might have missed a few places due to my
unfamiliarity with the code base, so if you find any, please let me
know.

Most of the paths that needed this trait, are in:

- Clap arguments, and that was handled by #[value_parse] attribute and a
  helper function.
- SocketMessage and that was handled by custom deserialization with the
  help of serde_with crate
2025-04-06 21:17:35 -07:00
Jerry Kingsbury
5a0196ac9d test(monitor.rs): ensure workspace count test
Created tests for the ensure_workspace_count function. The function is
tested by calling the function with only the default workspace, after
creating a workspace, and after we already have at least the number of
monitors passed into the funtion.
2025-04-05 12:14:10 -07:00
Jerry Kingsbury
46d0e340f9 test(monitor.rs): move container to workspace test
Created a test for the move_container_to_workspace funtion. The test
creates a workspace with 3 containers and an empty workspace, and
ensuresthat the container is moved to the correct workspace and that if
the workspace focus has changed when setting follow focus to true.
2025-04-05 12:14:10 -07:00
Jerry Kingsbury
371ef88ecb test(workspace): visible windows test
Created a test for the visible_windows function. The test creates two
windows in a workspace and checks to see if the the first created window
is the visible window. The test then maximizes the second window and
checks to ensure both windows are visible.

Expanded visible window test to ensure that a visible window will
display when adding another container.
2025-04-05 12:14:02 -07:00
dependabot[bot]
f5b5070436 chore(deps): bump openssl from 0.10.71 to 0.10.72
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.71 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-04 16:29:12 -07:00
LGUG2Z
c8320552b0 feat(cli): add session float rule cmds
This commit introduces three new commands, session-float-rule,
session-float-rules, and clear-session-float-rules, which add a
composite float rule for the currently focused window for the duration
of the komorebi session, print the float rules scoped to the current
komorebi session, and clear any float rules scoped to the current
komorebi session respectively.

The composite rule created is fairly strict, using
MatchingStrategy::Equals on the Exe, Class and Title.

Users can run session-float-rule as they are working to avoid having to
break their workflow and edit their configuration file, and when they
are ready, they can run session-float-rules to print out the composite
rules which have been generated and added to the current session to
further refine before adding them to their configuration files.

re #1402
2025-04-04 11:56:25 -07:00
alex-ds13
2a5a960c34 feat(wm): allow setting wallpaper per monitor
This commit adds the option to set `Wallpaper` per monitor. When
changing workspaces it will first check for a workspace wallpaper, if
there is none it then checks for a monitor wallpaper.
2025-04-04 11:12:47 -07:00
alex-ds13
10ab43a8ae fix(wm): properly update border colors
This commit changes the border manager notification to an enum that can
be a `Notification::Update(Option<isize>)` which should work like before
or a `Notification::ForceUpdate` which will always update the borders
and it will also update the border's brushes. To do so, this commit
moved the brush creation from the `create()` function to a new
`update_brushes` function on the `Border` itself and it changed the
`Border` `render_target` from a `OnceLock` to an `Option<RenderTarget>`
so that we can always change it when we need to.
This commit also adds a new function called `send_force_update()` to the
border manager to make it easier to send this notification.

This commit also added a check for `force_update_borders` on the
`process_command` function so that any command that reloads the
configuration, changes the theme or changes any border config can set it
to `true`. When this is true we send the `ForceUpdate` notification, if
false we send the normal `Update` notification as before.

The theme manager now always sends the `ForceUpdate` notification to the
border manager.

This commit adds a new function to the window manager called
`apply_wallpaper_for_monitor_workspace` which in turn calls the
`apply_wallpaper` function for the workspace specified with the
`monitor_idx` and `workspace_idx` given (if the workspace in question
doesn't have a wallpaper defined this won't do anything).

All these changes make it so the wallpaper theme generation colors are
properly applied in all situations.
2025-04-04 11:08:45 -07:00
alex-ds13
0e8ed8aa40 feat(wm): apply wallpapers per monitor 2025-04-04 11:08:45 -07:00
alex-ds13
fa2ccad5bf fix(wm): skip serde on WorkspaceGlobals 2025-04-04 11:08:45 -07:00
Csaba
3c4ccd2504 feat(bar): changing battery icons
This commit is changing the icon on the battery widget based on the
current level.

level | icon
------------
100 - 75: discharging
75 - 50: high
50 - 25: medium
25 - 10: low
10 - 0: warning

PR: #1398
2025-04-03 15:04:23 -07:00
LGUG2Z
7d821cd3db refactor(clippy): apply new rust lint fixes 2025-04-03 08:41:55 -07:00
Csaba
f4bbee0a2e feat(bar): auto select/hide widget based on value
This commit adds new settings to some widgets that allows to auto
select/hide them based on their current values.

The cpu/memory/network/storage widgets get a setting that auto selects
the widget if the current value/percentage is over a value.

The battery widget gets a setting that auto selects the widget if the
current percentage is under a value.

The storage widget gets a setting that auto hides the disk widget if the
percentage is under a value.

Also added 2 new settings (auto_select_fill and auto_select_text) to the
theme, in order to select the fill and text colors of an auto selected
widget.

(Easter egg: the network icons change if the value is over the limit)

PR: #1353
2025-04-03 07:42:27 -07:00
Lenus Walker
2934d011fd docs(mkdocs): add nvidia bar transparency workaround
Updated troubleshooting.md with the workaround for an issue where the
Komorebi Bar does not render transparency properly with Nvidia GPUs.
2025-04-02 17:22:06 -07:00
LGUG2Z
71762a5961 feat(cli): add datadir cmd
Because I'm tired of trying to find this stupid directory in
explorer.exe all the time.
2025-04-02 17:20:35 -07:00
zepocas
76aeefa9f7 feat(cli): add focused-workspace-layout query
This commit adds StateQuery::FocusedWorkspaceLayout variant to allow
queries for focused workspace layout via komorebic query.

This handles floating workspaces returning "None".
2025-04-02 15:33:46 -07:00
LGUG2Z
4968b0fe37 feat(themes): generate base16 palette from wallpaper
This commit adds a new Wallpaper configuration option to the
WorkspaceConfig, allowing the user to specify a path to a wallpaper
image file, and to specify whether to generate a base16 palette from the
colours of that image file.

Theme generation is enabled by default when a wallpaper is selected.

A set of theme options can be given to customize the colours of various
borders and accents.

The themes generated are also plumbed through to the komorebi-bar.

The palette generation algorithm from "flavours" (which has been forked
and updated) is quite slow, so the outputs are cached to file in
DATA_DIR, and keyed by ThemeVariant (Light or Dark).

The Win32 COM API to set the desktop wallpaper is also quite slow,
however this calls is async so it doesn't block komorebi's main thread.
2025-04-02 13:24:24 -07:00
LGUG2Z
b4b400b236 feat(themes): add custom base16 theme variant
This commit adds a custom Base16 theme variant and plumbs it throughout
the komorebi and komorebi-bar packages.
2025-04-02 13:23:31 -07:00
LGUG2Z
2ee0bbc0c7 refactor(themes): move colour.rs to komorebi-themes 2025-04-02 13:23:31 -07:00
Csaba
d38d3c956d feat(bar): add locked container widget, use accent colour for icons
This commit adds a new komorebi widget to indicate whether or not the
focused container is locked.

This commit also includes an icon colour change on the layer and layout
widgets to the accent colour.

The commit also renames the locked_window widget to locked_container as
it is more suitable.

PR: #1394
2025-04-02 13:23:02 -07:00
Csaba
052eb1c763 fix(bar): re-introduce retain exact workspace indices
This commit re-introduces a commit that was lost

original: 36e3eaad36

blame: bb31e7155d

fixes: https://github.com/LGUG2Z/komorebi/issues/1388
2025-04-01 10:08:22 -07:00
alex-ds13
58730b81b4 feat(gui): add floating and locked border colours 2025-04-01 05:53:33 -07:00
alex-ds13
274ae43e8f feat(wm): add unfocused_locked to border_colours
This commit adds the `unfocused_locked` color to the border_colours so
that users that don't use themes can still customize this color like
they do the others.
2025-04-01 05:53:26 -07:00
LGUG2Z
2a30f09bbd feat(cli): add version as state query variant
This commit adds a new StateQuery::Version, which allows integrators to
make decisions about how to handle different versions of komorebi's
state schema based on the version of komorebi that is running on a
user's machine.
2025-03-31 17:41:36 -07:00
alex-ds13
8fd18048a4 remove(wm): remove workspace_reconciliator 2025-03-31 15:23:33 -07:00
alex-ds13
5809735024 refactor(wm): refactor alt-tab reconciliation
This commit changes the way the alt-tab reconciliation is done.

It no longer uses the `workspace_reconciliator` with the sending of
notifications.

Instead there is a simple function that checks if the shown/uncloaked
window is already handled by komorebi and if it is it will check if it
is on an unfocused workspace and/or if it is an unfocused window of a
stacked container, returning the monitor/workspace index pair of said
window.

If we get this pair then we perform the reconciliation immediately by
focusing the monitor, workspace, container and window indices
corresponding to that window.
2025-03-31 15:23:14 -07:00
alex-ds13
96fdbbd1fb fix(client): re-export FloatingLayerBehaviour 2025-03-31 15:23:10 -07:00
Jerry Kingsbury
de131e9ca5 test(workspace): add more workspace tests
Added tests for the focus_container_by_window and
contains_managed_window functions.

test_focus_container_by_window crates two containers with different
distinct windows. The test checks to see if we are focused on the
expected container and window.

test_contains_managed_window creates two containers and checks to ensure
that when calling the function returns the expected result when checking
if a window is available in one of the containers.

Added a test for adding a new floating window. The test creates a
container with three windows and attempts to add one of the windows to
the list of floating windows, then checks to see if the first window in
the container was added to the floating window list and also checks the
list of windows and floating windows to ensure we get the expected
result.
2025-03-30 14:35:59 -07:00
LGUG2Z
07dba03255 docs(license): bump to 2.0.0 2025-03-30 12:59:41 -07:00
LGUG2Z
acd53dec1b feat(wm): add flag to set logging verbosity
This commit adds a --log-level flag to komorebi.exe which allows the
user to set the logging verbosity without setting the RUST_LOG
environment variable directly. If the RUST_LOG environment variable is
set, it will take precedence over the --log-level flag.
2025-03-28 19:03:07 -07:00
LGUG2Z
a98968d179 feat(config): add floating ws layer behaviour opt
This commit adds a new option to the WorkspaceConfig object,
floating_layer_behaviour, which allows the user to either set
FloatingLayerBehaviour::Tile or FloatingLayerBehaviour::Float. Although

I prefer Float as a default, there was a good enough argument to make
Tile the default based on the fact that the Floating layer is
automatically engaged based on the focused window, and previously when
the focused window was a floating window, new windows would be tiled
unless they matched floating rules.
2025-03-28 16:38:00 -07:00
LGUG2Z
8a32219867 docs(coc): move document from discord to github 2025-03-27 09:07:57 -07:00
Jerry Kingsbury
ce4b75cc3c test(monitor_reconciliator): add initial tests
Added a test for sending a MonitorNotification using the
send_notification function. The test sends a MonitorNotification and
tests that the notification was recieved using event_rx.

Added a test for inserting a monitor into the cache. The test creates
a monitor and uses the insert_in_monitor_cache function to insert the
monitor into cache, and will attempt to retrieve the monitor from the
cache and check to see if it matches the expected value.

Addaded a test for channel capacity. The test will send 20 notifications
and then another notification, then check to ensure that all 20
messages match the notification we sent. The test will also check to
ensure the last notification sent wasn't received.

Added a test for the notification listener. The test will create a
window manager and setup the notification listener, then check to see if
we can send a notification and receive the notification.

Created a Mock Monitor that can be used with testing functions that call
the Win32 API.

Added a test for the attach_display_devices function. The test will
create a Mock Monitor and add the monitor to a closure that simulates a
display_provider. The test passes the display_provider into the
attach_display_devices function and checks to ensure we recieve the
monitor we passed in.
2025-03-26 19:38:24 -07:00
alex-ds13
e4226ce623 feat(wm): show float windows in order on layer toggle
This commit raises and lowers all the floating windows so that they show
up with the smaller windows on top and the bigger windows on the bottom
when we toggle between layers.
2025-03-26 15:48:35 -07:00
alex-ds13
4bfd7febb4 feat(wm): focus floats depending on direction
This commit tries to focus the floating windows depending on the
direction used. It takes into account the top left corner of each window
and uses that to look for the closest top left corner in the direction
provided.

If there are no windows found in that direction, it then checks if it
can focus across monitor.
2025-03-26 15:48:15 -07:00
alex-ds13
5cc688dc6b feat(wm): track focused floating window
This commit changes the `floating_windows` from a `Vec<Window>` to a
`Ring<Window>` which allows us to keep track of the focused floating
window.

This combined with the existing layer switch allows us to know when we
should focus the focused container or the focused floating window.
2025-03-26 15:48:07 -07:00
LGUG2Z
d897890032 fix(bar): fall back to older pid icon lookups
This commit imports an older revision of my fork of windows-icons to
call when attempting to look up the icon of an application by it's
process id. This needs to be cleaned up before the next release.
2025-03-26 15:39:17 -07:00
LGUG2Z
e702d93a8a feat(cli): add move/send-to-last-workspace cmds
This commit adds two new komorebic commands, move-to-last-workspace and
send-to-last-workspace, which move or send the focused window to the
last focused workspace on the focused monitor.
2025-03-23 16:07:43 -07:00
LGUG2Z
a8c687d3d5 chore(deps): cargo update 2025-03-22 20:22:44 -07:00
alex-ds13
30fbc1ae73 feat(client): reexport win32_display_data
This commit reexports the `win32_display_data` crate so that any 3rd
party app that needs it can get it through the `komorebi-client` without
having to keep manually synchronizing it with the version used by komorebi.
2025-03-22 19:35:23 -07:00
LGUG2Z
cb60e91842 feat(bar): show icons for uwp apps
This commit integrates the excellent investigation and work done by
@davor-skontra on the windows-icons repo to enable the retrieval of UWP
applications, including all those annoying Microsoft applications which
all share the ApplicationFrameHost.exe executable and the
ApplicationFrameWindow class.

Since these applications share the same executable, the icon cache in
komorei-bar has been updated to use the window hwnd as a key intead of
the window executable.

resolve #1226
2025-03-22 19:31:30 -07:00
LGUG2Z
64d29d606a refactor(wm): add dep injection to monitor reconiliator
This commit adds some dependency injection to the monitor reconciliator
module to make it easier to test the behaviour when different kinds of
data are returned from win32_display_data.
2025-03-22 13:16:15 -07:00
LGUG2Z
072a62c314 perf(wm): clone-free locked deque insert/remove
This commit replaces my initial clumsy LockedDeque insert/remove
implementations with Clone-free implementations by @alex-ds13.
2025-03-22 12:33:40 -07:00
LGUG2Z
a95e6e9644 refactor(wm): consolidate handling of ws container insertion/removal 2025-03-22 12:33:39 -07:00
LGUG2Z
6ba19d3ea2 feat(wm): add locked containers per workspace
This commit adds the concept of locked container indexes to komorebi
workspaces.

When a container index is locked, it can only be displaced by manual
user actual - usually when another container is moved there, and when
this happens, that container becomes the locked container.

In the locked state, the container at the locked index should never be
displaced by new windows opening or existing windows around it being
closed.

When the total number of containers on a workspace falls below the
number of the locked index, the locked index will be removed.

A locked index can be identified by a special border color linked to the
new WindowKind::UnfocusedLocked variant.

The implementation of locked container indexes is backed by a new data
structure called a LockedDeque, which is a VecDeque with an auxiliary
HashSet which keeps track of locked indices.

A new komorebic command "toggle-lock" has been added to support
programmatic use of this feature, as well as the
LockMonitorWorkspaceContainer and UnlockMonitorWorkspaceContainer
SocketMessage variants which can be used by status bars.
2025-03-22 12:28:15 -07:00
LGUG2Z
edf1943157 chore(dev): begin v0.1.36-dev 2025-03-22 12:14:43 -07:00
LGUG2Z
d0c847e5bc docs(mkdocs): fix some broken relative links 2025-03-22 11:55:36 -07:00
LGUG2Z
992bc2abfe chore(release): v0.1.35 2025-03-20 21:17:55 -07:00
Jerry Kingsbury
fa07f2d2f8 test(monitor): add tests for ws and containers fns
Added tests around adding and removing workspaces and containers.
2025-03-20 18:36:40 -07:00
Jerry Kingsbury
cc4e204191 test(container): add tests for addition and removal
Added tests for the various add, remove, and contains functions.
2025-03-20 18:35:55 -07:00
alex-ds13
24791f0ce5 fix(wm): change when floating windows are centered
This commit changes the behaviour of when floating windows are centered
by making them only center when we are on a `WorkspaceLayer::Floating`
and the window doesn't match a `floating_windows` rule and the workspace
is not a floating workspace.
2025-03-20 18:34:35 -07:00
LGUG2Z
6b95bf95f9 fix(wm): unset all duplicate monitor serial ids
This commit fixes a rare issue, seen exclusively with Acer monitors so
far, where two monitors of the same model can have an identical serial
number id.

If we encounter a system which has two connected monitors with the same
serial id number, the serial id number will be forcefully unset and
blacklisted for the rest of the session.

In this case, users must fall back to using device_id for options like
display_index_preferences.

Possibly a little overkill, but since this has been such a headache I'm
going to opt for this approach over #1368 for now.
2025-03-20 09:08:47 -07:00
LGUG2Z
c0e1e9366d chore(deps): bump windows-rs from 0.60 to 0.61 2025-03-18 08:54:55 -07:00
Jerry Kingsbury
532436fe1a test(workspace): add container and window tests
Added tests for functions related to removing, adding, and focusing a
container and windows.
2025-03-18 08:48:11 -07:00
LGUG2Z
532949409c fix(borders): show regular cursor on hover
This commit ensures that the WM_SETCURSOR message is handled by border
windows by setting the cursor to the default IDC_ARROW, which prevents
the spinning "Loading" icon from showing on hover.

Some credit is due to the user steiner5 on Discord for whining about me
not being interested in working on this while more generally complaining
about komorebi, which consequently made me want to work on this out of
spite since he's probably going to bounce off the software anyway.
2025-03-17 19:18:40 -07:00
dependabot[bot]
ec4a5e6491 chore(deps): bump reqwest from 0.12.12 to 0.12.14
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.12 to 0.12.14.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/v0.12.14/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.12...v0.12.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 19:00:16 -07:00
dependabot[bot]
f6e99eaac1 chore(deps): bump uuid from 1.15.1 to 1.16.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.15.1...v1.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 19:00:09 -07:00
dependabot[bot]
a6cf801a6b chore(deps): bump starship-battery from 0.10.0 to 0.10.1
Bumps [starship-battery](https://github.com/starship/rust-battery) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/starship/rust-battery/releases)
- [Changelog](https://github.com/starship/rust-battery/blob/main/CHANGELOG.md)
- [Commits](https://github.com/starship/rust-battery/compare/v0.10.0...v0.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 19:00:02 -07:00
Jerry Kingsbury
83d11c6f0f test(workspace): add container manipulation tests
Added tests for the different container adding and removing functions.

Also added a test for the contains_window function.
2025-03-17 18:32:47 -07:00
Jerry Kingsbury
1ba1c57ba0 test(wm): add cycle window tests
Added a test for the cycle_window_in_direction function. The function is
tested by creating 3 windows and cycling in both the next a previous
direction. The test will ensure that the windows were cycled by checking
the window index to ensure it is the expected window.

Created a test for cycling the window by index. This test is similar to
the other cycle window test and performs the same steps as that one,
except it uses the test_cycle_container_index_in_direction function
instead.
2025-03-17 18:20:47 -07:00
alex-ds13
9f16894a09 fix(wm): correct workspace restore + remove workarounds
This commit removes some workarounds on the `update_focused_workspace`
function that were there to fix issues related to some bugs on the
`workspace.restore()` function.

This commit fixes the bugs on the `restore` function instead. The
`update_focused_workspace` function should be used only to update a
workspace layout.
2025-03-17 18:17:30 -07:00
alex-ds13
df9ae931cc feat(wm): toggle float override with floating layer
This commit ensures that when the user switches to the flooating layer
any new spawned window will be spawned floating as if the float override
was set.

This results in a workflow which makes it easier to spawn floating
windows on the fly.

This effectively reverts commit ff2aa5e51a.
2025-03-17 18:16:16 -07:00
alex-ds13
be2af9fdcb fix(borders): update monitor_idx on all borders
This fixes the issue where borders moved to other monitors would
sometimes be removed by their initial monitor because this data wasn't
being updated.

This effectively reverts commit 5919f88b38.
2025-03-17 18:15:27 -07:00
alex-ds13
c083484ef0 fix(wm): correctly focus workspace on eager-focus 2025-03-17 18:12:51 -07:00
Jerry Kingsbury
1804b21c4a test(wm): add tests for ws movement + tiling state
Added a test for moving a workspace from one monitor to another. The
test will ensure that after calling the function, the current monitor
index is the one where the workspace was transfered to. Test also will
check the count to ensure the new workspace was added to the new monitor
and that the old monitor has one less.

Added a test for swapping monitor workspaces. The test will create two
monitors one with a workspace that has a container containing multiple
windows and another with one. The test calls the swap_monitor_workspaces
function and then checks to see if the two workspaces were successfully
swapped.

Added a test to test toggling the tiling state. After each switch the
test will test the current state to ensure that it is the expected
state.
2025-03-16 15:55:00 -07:00
LGUG2Z
b6bd191cf5 feat(wm): center windows spawned during float override
It's very annoying and unclear when enabling the float override to open
the next window as floating to have that window appear immediately over
an existing tile.

This commit sets an explicit behaviour to center windows spawned while
the float override is active.
2025-03-16 15:48:28 -07:00
LGUG2Z
5919f88b38 fix(borders): do multiple render passes when required
This commit addresses a border rendering issue when moving a window from
a higher-indexed monitor to a lower-indexed monitor.

Previously, we would do a single render pass across all monitors in
order of their indexes, destroying borders no longer needed, and
creating new borders for new windows.

This resulted in the window being moved to the lower-indexed monitor
still existing in the global border cache when that monitor's borders
were updated, but then being removed when the borders of the origin,
higher-indexed monitor were updated.

With the changes in this commit, if we encounter a situation like this,
an additional render pass will be executed to ensure that the window
will have a corresponding border created on the destination
lower-indexed monitor.
2025-03-16 14:33:22 -07:00
LGUG2Z
ff2aa5e51a feat(wm): toggle float override with floating layer
This commit ensures that a workspace float override will also be applied
when the user switches to the floating layer, and removed when the user
switches to the tiling layer. This results in a workflow which makes it
easier to spawn floating windows on the fly.
2025-03-16 13:32:18 -07:00
LGUG2Z
42c12d5ec3 docs(mkdocs): add initial pages of usage section 2025-03-15 20:28:14 -07:00
LGUG2Z
f0ce8e8572 fix(wm): focus when switching to ws with only floating windows
This commit ensures that if a user switches from a workspace with
managed windows to a workspace without any managed windows but only
floating windows, the focused window from the previous workspace will
lose focus as it should, and the first floating window on the new
workspace will gain focus as it should.
2025-03-15 13:34:31 -07:00
Csaba
bdea4821c3 refactor(bar): move widgets to new folder
This commit moves all the widgets to a new folder in order to organize
the code while hoping to make it easier to find and add a widget.
2025-03-15 09:47:45 -07:00
Jerry Kingsbury
229aeb7ddc test(wm): add tests for additions, removals and swaps
Added a test for removing a window from a container. The test checks to
ensure a new container is created and that the removed window from the
original container is added to the new container. Also ensures that the
old container has one less container after the function is called.

Added a test for removing a workspace. The test keeps track of the
number of workspaces and the current workspace index.

Added test for swapping containers from different monitors. The test
will create two monitors and containers with different amount of
windows. Test will verify that the swap is successful by checking that
the number of windows in the container matches the number of windows it
was created with in the previous monitor. The same will be done with the
other container.
2025-03-15 09:45:57 -07:00
alex-ds13
17cbdc8663 docs(mkdocs): add multi monitor common-workflows section 2025-03-14 16:51:02 -07:00
Csaba
9f3d806f79 fix(bar): limit new state freq on time and date
This commit limits the number of times the time and date widgets get a
new state.

Even though having a limit of 1 second on the ui repaint, when the bar
is focused or hovered, this is ignored and state of these widgets were
updated many more times a second.

The time widget looked less accurate and lagging (especially with
multiple showing seconds) so the refresh interval is set to be 500 ms
instead of 1 second.
2025-03-14 15:59:51 -07:00
LGUG2Z
fe9a1416e7 feat(config): allow multiple asc files
This commit allows either the single canonical applications.json file,
or multiple files which adhere to the asc scheme to be given to the
app_specific_configuration_path config option.

I thought I had already implemented this earlier, but evidently I
hadn't.

This will be useful for people who want to maintain their own
independent set of asc rules, as they can be kept in a dedicated file
which won't be overwritten by the fetch-asc command.

resolve #736
2025-03-13 19:35:14 -07:00
Csaba
3618beb366 fix(bar): add focus-monitor-at-cursor msg to buttons
This commit adds the FocusMonitorAtCursor SocketMessage on all the
toggle buttons as the monitor idx is not sent as a parameter and the
monitor needs to be focused when these buttons are clicked on the bar
(especially for multiple bars).
2025-03-13 16:57:32 -07:00
alex-ds13
a4de2ee841 fix(wm): use preferred id when caching monitors
Use the preferred id set by the user as the key when caching monitors.
This way if a user is having issues with the device_id on their system
they can use the serial_number_id on display_index_preferences.

If instead a user is having issues with the serial_number_id they can
make sure to use the device_id on display_index_preferences. Komorebi
will use the preferred one as a key when caching the monitors.

This PR also includes a change of the DISPLAY_INDEX_PREFERENCES from a
Mutex to RwLock.

I believe the RwLock is better since it allows multiple readers at the
same time, while the mutex blocks on all calls even if you just want to
read or compare some value.

For display index preferences (same thing applies to other existing
mutexes) most times we access it is a read-only access. We only ever
change it when applying a config or when a command is used to change it.

Every other use is a read-only use so it doesn't make sense to block on
those situations
2025-03-13 09:12:15 -07:00
alex-ds13
60e1834b43 fix(wm): correct float window move/resize
This commit fixes an issue where the move/resize functions for floating
windows weren't properly taking into account the coordinates of
secondary monitors and were only working correctly on the main monitor
where top/left was 0/0.
2025-03-11 17:58:05 -07:00
alex-ds13
54323c4c6a fix(wm): update layer on cross monitor moves 2025-03-11 15:19:27 -07:00
LGUG2Z
6516c808ee chore(deps): cargo update 2025-03-10 20:52:42 -07:00
Jerry Kingsbury
894b6f3d96 test(wm): add container transfer and cycle tests
Implemented drop to ensure that the socket is deleted whenever the
function goes out of scope. This will ensure that if the test fails, the
test socket file will still be removed.

Added a test for cycling to the next and previous container in a
workspace.

Added a test for transfering a window from Monitor 1 to Monitor 2

Added a test for transfering a container to another montior. Also
updated the transfer window test to transfer the window back to the
original container. Both tests will check both monitors to ensure the
expected number of containers are in both.
2025-03-10 20:43:55 -07:00
dependabot[bot]
7ccdff4986 chore(deps): bump netdev from 0.32.0 to 0.33.0
Bumps [netdev](https://github.com/shellrow/netdev) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/shellrow/netdev/releases)
- [Commits](https://github.com/shellrow/netdev/compare/v0.32.0...v0.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 08:33:09 -07:00
dependabot[bot]
c48e1db0ff chore(deps): bump random_word from 0.4.3 to 0.5.0
Bumps [random_word](https://github.com/MitchellRhysHall/random_word) from 0.4.3 to 0.5.0.
- [Commits](https://github.com/MitchellRhysHall/random_word/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 08:32:59 -07:00
alex-ds13
ea9752d5e1 fix(wm): focus correct window if monocled stack
Previously if we had a stack on a monocle container and tried to cycle
stack or move the window within the stack or even using the focus stack
window from a bar it would focus the wrong window and temporarely show
that wrong window. This commit fixes this.
2025-03-10 08:23:49 -07:00
Csaba
acf780767c feat(bar): add changing icons based on the time
This commit adds a little Easter egg on the time widget.

Use the `changing_icon` setting to enable this feature.

Based on the current time, the widget will use different icons to
indicate certain activities of the day.

00:00 MOON
06:00 ALARM
06:01 BREAD
06:30 BARBELL
08:00 COFFEE
08:30 CLOCK
12:00 HAMBURGER
12:30 CLOCK_AFTERNOON
18:00 FORK_KNIFE
18:30 MOON_STARS
2025-03-09 11:48:46 -07:00
Jerry Kingsbury
8e588d0284 test(wm): add workspace and monitor tests
Added tests to test switching monitor focus, setting workspace name, and
monitor size.
2025-03-08 13:49:51 -08:00
dependabot[bot]
91ff9b8852 chore(deps): bump ring from 0.17.11 to 0.17.13
Bumps [ring](https://github.com/briansmith/ring) from 0.17.11 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 16:47:46 -08:00
Csaba
81a7951312 feat(bar): added timezone to time and date widgets
This commit adds the timezone on the time and date widgets as a new
setting.

In case the timezone is invalid, the output is replaced with an error
message.

Use a custom format to display additional information.

resolve #1312
2025-03-07 16:28:45 -08:00
LGUG2Z
555308db5f test(wm): don't rely on datadir for socket location
This commit avoids relying on DATA_DIR for the test socket location as
this path is not available to us in GitHub Actions runners.
2025-03-07 16:10:16 -08:00
LGUG2Z
c90769f5fa chore(deny): allow unmaintained crate paste 2025-03-07 16:01:41 -08:00
LGUG2Z
76002385ab test(wm): colocate tests with src files, add socket msg test 2025-03-06 19:39:59 -08:00
Jerry Kingsbury
f40e80cd61 test(wm): add window manager unit tests
Created a test that creates the WM instance and ensures the instance is
running. The test creates a custom socket and then cleans up the socket
file after completion.

Created a test that creates a WM instance, monitor instance, and
workpace. The tests checks to ensure that the expected workspace is
focused properly.

Included recommended fixes to ensure that the focus_workspace function
is used correctly and that the test accurately checks the workspaces
length, current workspace index, and switching to an existing workspace.
2025-03-06 15:34:30 -08:00
LGUG2Z
e4f9d8af86 chore(deps): add deny.toml and dependencies.json 2025-03-06 15:34:27 -08:00
LGUG2Z
0c64432c25 feat(cli): add cycle-empty-workspace cmd
This commit adds a variation of the cycle-workspace command which will
attempt to focus the next empty workspace in the given direction (with
the usual wraparound). If there are no empty workspaces available, this
command will do nothing.
2025-03-05 15:52:21 -08:00
LGUG2Z
fe20caa56a fix(client): expose schemars feature and allow opt-out 2025-03-05 15:49:46 -08:00
Csaba
02a2796e7d fix(bar): correct time widget binary format alignment
This commit adds a bit of offset to the time widget's binary clocks so
they are more in the middle.

It also fixes some visual changes that were caused by upgrading to
eframe 0.31
2025-03-04 14:57:21 -08:00
LGUG2Z
a0eb025cec fix(bar): add opt schemars derive on test case 2025-03-04 08:39:47 -08:00
alex-ds13
70a61376a8 fix(borders): ignore borders on all stack window updates 2025-03-04 08:20:35 -08:00
LGUG2Z
1761919707 perf(client): disable schemars feature on komorebi lib dep 2025-03-03 21:49:43 -08:00
LGUG2Z
1325da4e81 chore(deps): cargo update 2025-03-03 21:46:03 -08:00
alex-ds13
0776ca1565 feat(border): cache borders on all workspaces
This commit refactors the `border_manager` with the following changes:

- Rework the way the borders are created and their pointer is sent to
  the window message handler. Now we also store the same pointer as a
  `Box<Border>` on the `border_manager`'s `BORDER_STATE`. This means
  that the borders we have are exactly the same ones that the border
  window itself is accessing so we can now store the border's info
  inside the `Border` struct and all of it will be accessible by the
  border window as well. This makes it so the "ACTUAL" border struct is
  the one created on the thread of the `Border::create()` function and
  when that thread finishes (after the border window closes) it will
  handle the drop of the border itself. However this means we need to be
  careful with our own stored `Box<Border>` since it will point to the
  same memory we can't let the compiler dropped them as usual or
  otherwise it would create heap corruption errors. So this commit
  creates a special function called `destroy_border()` to actually close
  the window border without dropping the border, since it will be later
  dropped by the thread that created it.

- Remove `BORDERS_MONITORS`, `FOCUS_STATE` and `RENDER_TARGETS` arc
  mutexes, since now this info is stored on the border itself.

- Change the `BORDER_STATE` to now map an id (container or window) to a
  `Box<Border>`.

- Change the `WINDOWS_BORDERS` to now map a window hwnd to a border id.

- Create new struct `BorderInfo` which as the border window hwnd and the
  border kind. This struct is what is now returned by the function
  `window_border()` which checks if some window as a border attached to
  it and if it does it returns this info. There is no need to clone the
  entire border. If in the future we need more info we can just add it
  to this struct.

- Change the way we clear the `BORDER_STATE`. Like mentioned before we
  need to be sure we don't drop the `Box<Border>` when removing it, so
  now we use the `.drain` function to remove all the borders as an
  iterator and we call the `destroy_border()` on each border we are
  removing.

- We now check if a border's `tracking_hwnd` has changed and if it does
  we simply update it instead of destroying the border and create a new
  one.

- Create function `delete_border` so that we can remove a border
  properly from outside the `border_manager`.

- Create function `hide_border` which spawns a new thread that searches
  if a window hwnd has a border attached to it and if it does it hides
  said border window. This function is called on every `window.hide()`.

- Create function `show_border` which spawns a new thread that searches
  if a window hwnd has a border attached to it and if it does it restores
  said border window. This function is called on every `window.restore()`.

- This commit also changes the previous `window.hide()` and
  `window.restore()` functions to be named:
  - `window.hide_with_border(hide_border: bool)`: this is the same
    function as before but adds a check at the end in case `hide_border`
    is true it calls `border_manager::hide_border()`. A new function was
    created with the same name as before `window.hide()` which by
    default calls this new function with `hide_border = true`.
  - `window.restore_with_border(restore_border: bool)`: this is the same
    function as before but adds a check at the end in case `hide_border`
    is true it calls `border_manager::hide_border()`. A new function was
    created with the same name as before `window.hide()` which by
    default calls this new function with `hide_border = true`.

- This commit creates a new function on `Container` called
  `load_focused_window_ignore_borders()` which performs the same as
  `load_focused_window()` but it ignores the borders when hiding and
  restoring the windows. This function, along with the
  `hide_with_border(false)` and `restore_with_border(false)` are used on
  all functions related to changing focus on a stack since if we let the
  borders be hidden and restored when cycling or changing focus on a
  stack the border would flicker slightly, this prevents that. Ignore
  borders when clicking on the stackbar as well.
  (P.S. there might still be other places that I forgot to use these new
  functions, but if that is the case then what will happen is a simple
  flicker of the stack border...)

- The `remove_window` from `Workspace` needs to call the
  `border_manager::delete_border()` so that wew make sure we remove that
  windows's border window as well if it exists. This is essential when
  enforcing workspace rules, otherwise the border would be left behind.

- Lastly, but not least, now that we hide the borders windows along with
  their tracking window, we no longer remove the borders when swapping
  workspaces or when toggling monocle, etc. Instead we keep all borders
  of all workspaces cached and simply hide them. They are only removed
  when their tracking window is closed or cloaked on a stack (since on a
  stack we only keep one border for all the entire stack container).
  This means that when changing between workspaces we no longer see the
  borders showing up delayed after the windows show up. Now both the
  window and it's border show up as if they are one and the same.
2025-03-03 21:15:45 -08:00
alex-ds13
724b0b7692 fix(border): update border when moving from admin windows 2025-03-03 21:13:37 -08:00
LGUG2Z
b53de81754 perf(cargo): make schemars derives optional
This commit makes all schemars::JsonSchema derives optional. After
analyzing the output of cargo build timings and llvm-lines, it was clear
that the majority of the 2m+ incremental dev build times was taken up by
codegen, and the majority of it by schemars.

Developers can now run cargo commands with --no-default-features to
disable schemars::JsonSchema codegen, and all justfile commands have
been updated to take this flag by default, with the exception of the
jsonschema target, which will compile with all derives required to
export the various jsonschema files.

Incremental dev build times for komorebi.exe on my machine are now at
around ~18s, while clean dev build times for the entire workspace are at
around ~1m.
2025-03-03 21:12:46 -08:00
Csaba
a837fea40c feat(bar): add icons to workspace-layer widget
This commits adds the ability to set icons for the `workspace-layer` with
the `DisplayFormat` and a setting to specify if it should `show_when_tiling`
or not.

collab with @alex-ds13
2025-03-03 08:13:29 -08:00
LGUG2Z
dd577c0eb3 fix(wm): preserve resize dimensions on offset toggle
This commit ensures that the resize dimensions will be reserved for
other monitors and workspaces when the
toggle-window-based-work-area-offset command is used.
2025-03-03 08:13:29 -08:00
Csaba
7d497c3e14 fix(bar): always add stroke on selected_frame
This commit fixes a breaking change on the selected_frame that was
introduced by eframe version 0.31.

In this version, the stroke is drawn on the inside of a Frame instead it
being drawn on the outside like before.

This now means that a stroke needs to be added on all the states of the
Frame in order to avoid all the elements to be moving around on hover.
2025-03-03 08:13:29 -08:00
LGUG2Z
e6398c29f8 docs(readme): add active individual commercial use licenses count 2025-02-27 18:15:43 -08:00
167 changed files with 35930 additions and 15144 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -55,6 +55,7 @@ body:
label: Hotkey Configuration
description: >
Please provide your whkdrc or komorebi.ahk hotkey configuration file
render: shell
- type: textarea
validations:
required: true
@@ -62,3 +63,4 @@ body:
label: Output of komorebic check
description: >
Please provide the output of `komorebic check`
render: shell

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Check and close feature issues
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const issue = context.payload.issue;

View File

@@ -13,11 +13,19 @@ on:
- hotfix/*
tags:
- v*
schedule:
- cron: "30 0 * * 0" # Every day at 00:30 UTC
# schedule:
# - cron: "30 0 * * 0" # Every day at 00:30 UTC
workflow_dispatch:
jobs:
cargo-deny:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: EmbarkStudios/cargo-deny-action@v2
build:
strategy:
fail-fast: true
@@ -35,10 +43,11 @@ jobs:
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- run: rustup toolchain install stable --profile minimal
- run: rustup component add --toolchain stable-x86_64-pc-windows-msvc clippy
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
- uses: Swatinem/rust-cache@v2
with:
@@ -47,7 +56,7 @@ jobs:
key: ${{ matrix.platform.target }}
- run: cargo +nightly fmt --check
- run: cargo clippy
- run: cargo test --package komorebi --test compat
- run: cargo test
- uses: houseabsolute/actions-rust-cross@v1
with:
command: "build"
@@ -56,7 +65,7 @@ jobs:
- run: |
cargo install cargo-wix
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: |
@@ -73,12 +82,12 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -120,14 +129,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -162,14 +171,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.ref_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi

6
.gitignore vendored
View File

@@ -6,3 +6,9 @@ dummy.go
komorebic/applications.yaml
komorebic/applications.json
/.vs
/bar-schema
/komorebi-schema
/.wrangler
/.xwin-cache
result
/.direnv

40
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,40 @@
# The Komorebi Code of Conduct
This document is based on the [Rust Code of
Conduct](https://www.rust-lang.org/policies/code-of-conduct)
## Conduct
- We are committed to providing a friendly, safe and welcoming environment for
all, regardless of level of experience, gender identity and expression, sexual
orientation, disability, personal appearance, body size, race, ethnicity, age,
religion, nationality, or other similar characteristic.
- Please avoid using overtly sexual aliases or other nicknames that might
detract from a friendly, safe and welcoming environment for all.
- Please be kind and courteous. Theres no need to be mean or rude.
- Respect that people have differences of opinion and that every design or
implementation choice carries a trade-off and numerous costs. There is seldom a
right answer.
- Please keep unstructured critique to a minimum. If you have solid ideas you
want to experiment with, make a fork and see how it works.
- We will exclude you from interaction if you insult, demean or harass anyone.
That is not welcome behavior. We interpret the term “harassment” as including
the definition in the [Citizen Code of
Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md);
if you have any lack of clarity about what might be included in that concept,
please read their definition. In particular, we dont tolerate behavior that
excludes people in socially marginalized groups.
- Private harassment is also unacceptable. No matter who you are, if you feel
you have been or are being harassed or made uncomfortable by a community member,
please contact me immediately. Whether youre a regular contributor or a
newcomer, we care about making this community a safe place for you and weve got
your back.
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing
behavior is not welcome.

4542
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,27 +2,29 @@
resolver = "2"
members = [
"komorebi",
"komorebi-client",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes"
"komorebi",
"komorebi-client",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes",
"komorebi-shortcuts",
]
[workspace.dependencies]
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono-tz = "0.10"
chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.31"
egui_extras = "0.31"
eframe = "0.33"
egui_extras = "0.33"
dirs = "6"
dunce = "1"
hotwatch = "0.5"
schemars = "0.8"
schemars = "1.1"
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
@@ -31,57 +33,55 @@ strum = { version = "0.27", features = ["derive"] }
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
parking_lot = "0.12"
paste = "1"
sysinfo = "0.33"
sysinfo = "0.37"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "55cebdebfbd68dbd14945a1ba90f6b05b7be2893" }
windows-numerics = { version = "0.1" }
windows-implement = { version = "0.59" }
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
windows-numerics = { version = "0.3" }
windows-implement = { version = "0.60" }
windows-interface = { version = "0.59" }
windows-core = { version = "0.60" }
windows-core = { version = "0.62" }
shadow-rs = "1"
which = "7"
which = "8"
[workspace.dependencies.windows]
version = "0.60"
version = "0.62"
features = [
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control"
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control",
]
[profile.dev-jeezy]
inherits = "dev"
debug = false
opt-level = 1
[profile.dev-jeezy.package."*"]
opt-level = 3
[profile.release-jeezy]
[profile.release-opt]
inherits = "release"
incremental = true
codegen-units = 256
lto = true
panic = "abort"
codegen-units = 1
strip = true
[workspace.metadata.crane]
name = "komorebi-workspace"

View File

@@ -1,6 +1,6 @@
# Komorebi License
Version 1.0.0
Version 2.0.0
## Acceptance
@@ -13,9 +13,20 @@ your licenses.
The licensor grants you a copyright license for the software
to do everything you might do with the software that would
otherwise infringe the licensor's copyright in it for any
permitted purpose. However, you may only make changes according
permitted purpose. However, you may only distribute the source
code of the software according to the [Distribution License](
#distribution-license), you may only make changes according
to the [Changes License](#changes-license), and you may not
distribute the software or new works based on the software.
otherwise distribute the software or new works based on the
software.
## Distribution License
The licensor grants you an additional copyright license to
distribute copies of the source code of the software. Your
license to distribute covers distributing the source code of
the software with changes permitted by the [Changes License](
#changes-license).
## Changes License
@@ -45,7 +56,7 @@ law. These terms do not limit them.
These terms do not allow you to sublicense or transfer any of
your licenses to anyone else, or prevent the licensor from
granting licenses to anyone else. These terms do not imply
granting licenses to anyone else. These terms do not imply
any other licenses.
## Patent Defense
@@ -63,7 +74,7 @@ violated any of these terms, or done anything with the software
not covered by your licenses, your licenses can nonetheless
continue if you come into full compliance with these terms,
and take practical steps to correct past violations, within
32 days of receiving notice. Otherwise, all your licenses
32 days of receiving notice. Otherwise, all your licenses
end immediately.
## No Liability
@@ -88,11 +99,10 @@ organizations that have control over, are under the control of,
or are under common control with that organization. **Control**
means ownership of substantially all the assets of an entity,
or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the
software under these terms.
**Use** means anything you do with the software requiring one
of your licenses.

View File

@@ -7,9 +7,9 @@ Tiling Window Management for Windows.
<img alt="Tech for Palestine" src="https://badge.techforpalestine.org/default">
</a>
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LGUG2Z/komorebi/.github/workflows/windows.yaml">
<img alt="GitHub" src="https://img.shields.io/github/license/LGUG2Z/komorebi">
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/LGUG2Z/komorebi/total">
<img alt="GitHub commits since latest release (by date) for a branch" src="https://img.shields.io/github/commits-since/LGUG2Z/komorebi/latest">
<img alt="Active Individual Commercial Use Licenses" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Flgug2z-ecstaticmagentacheetah.web.val.run&query=%24.&label=active%20individual%20commercial%20use%20licenses&cacheSeconds=3600&link=https%3A%2F%2Flgug2z.com%2Fsoftware%2Fkomorebi">
<a href="https://discord.gg/mGkn66PHkx">
<img alt="Discord" src="https://img.shields.io/discord/898554690126630914">
</a>
@@ -29,6 +29,46 @@ Tiling Window Management for Windows.
![screenshot](https://user-images.githubusercontent.com/13164844/184027064-f5a6cec2-2865-4d65-a549-a1f1da589abf.png)
## Note: Students using devices enrolled in mobile device management (MDM)
Your usage still falls under the [Komorebi License 2.0.0](./LICENSE.md).
You can email me at the address I sign my commits with (add `.patch` to the end
of any commit URL on GitHub to find it) from the address associated with your
institution with the subject "komorebi - student with an MDM device", and I will
be able to remove the splash intended for corporate users, whose usage falls
under the [Individual Commercial Use
License](https://lgug2z.com/software/komorebi).
This is currently a manual process - most days this shouldn't take more than
12h, and you will receive an email reply from me when the process is complete.
If you haven't had a reply to your email within 24h you can reach out to me on
Discord.
## Note: Unexpected mobile device management (MDM) detection prompts
You have most likely unintentionally enrolled your device in "Bring Your Own
Device" (BYOD) MDM. You can confirm if this is the case by running `dsregcmd
/status` and then take the appropriate steps to remove the MDM profile and take
back full control of your system.
If you need help doing this you can ask on Discord.
## Note: komorebi for Mac
If you made your way to this repo looking for [komorebi for
Mac](https://github.com/KomoCorp/komorebi-for-mac), the project is currently
being developed in private with [early access available to GitHub
Sponsors](https://github.com/sponsors/LGUG2Z).
If you want to see how far along development is before signing up for early
access (spoiler: it's very far along!) there is an overview video you can watch
[here](https://www.youtube.com/watch?v=u3eJcsa_MJk).
Sponsors with early access can install komorebi for Mac either by compiling
from source, by using Homebrew, or by using the project's Nix Flake.
## Overview
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
@@ -71,7 +111,10 @@ showcases the many awesome projects that exist in the _komorebi_ ecosystem.
## Licensing for Personal Use
`komorebi` is licensed under the [Komorebi 1.0.0
`komorebi` is [educational source
software](https://lgug2z.com/articles/educational-source-software/).
`komorebi` is licensed under the [Komorebi 2.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
@@ -82,7 +125,7 @@ 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
The [Komorebi 2.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
@@ -99,7 +142,8 @@ 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.
YouTube, gain the ability to submit feature requests on the issue tracker, and
receive releases of komorebi with "easter eggs" on physical media.
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
@@ -142,7 +186,8 @@ video will answer the majority of your questions.
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
unfocused window transparency and animations enabled, using a custom status bar integrated using
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
_komorebi_'
s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
@@ -389,7 +434,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.34"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.39"}
use anyhow::Result;
use komorebi_client::Notification;

432
check_schema_docs.py Normal file
View File

@@ -0,0 +1,432 @@
#!/usr/bin/env python3
"""
Check schema.json and schema.bar.json for missing docstrings and map them to Rust source files.
This script analyzes the generated JSON schemas and identifies:
1. Type definitions ($defs) missing top-level descriptions
2. Enum variants missing descriptions (in oneOf/anyOf)
3. Enum variants missing titles (object variants in oneOf/anyOf)
4. Struct properties missing descriptions
5. Top-level schema properties missing descriptions
For each missing docstring, it attempts to find the corresponding Rust source
file and line number where the docstring should be added.
"""
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass
class MissingDoc:
type_name: str
kind: str # "type", "variant", "property", "variant_title"
item_name: Optional[str] # variant or property name
rust_file: Optional[str] = None
rust_line: Optional[int] = None
def __str__(self):
location = ""
if self.rust_file and self.rust_line:
location = f" -> {self.rust_file}:{self.rust_line}"
elif self.rust_file:
location = f" -> {self.rust_file}"
if self.kind == "type":
return f"[TYPE] {self.type_name}{location}"
elif self.kind == "variant":
return f"[VARIANT] {self.type_name}::{self.item_name}{location}"
elif self.kind == "variant_title":
return f"[VARIANT_TITLE] {self.type_name}::{self.item_name}{location}"
else:
return f"[PROPERTY] {self.type_name}.{self.item_name}{location}"
@dataclass
class SchemaConfig:
"""Configuration for a schema to check."""
schema_file: str
search_paths: list[str]
display_name: str
def find_rust_definition(
type_name: str, item_name: Optional[str], kind: str, search_paths: list[Path]
) -> tuple[Optional[str], Optional[int]]:
"""Find the Rust file and line number for a type/variant/property definition."""
if kind == "type":
patterns = [
rf"pub\s+enum\s+{type_name}\b",
rf"pub\s+struct\s+{type_name}\b",
]
elif kind in ("variant", "variant_title"):
patterns = [
rf"^\s*{re.escape(item_name)}\s*[,\(\{{]",
rf"^\s*{re.escape(item_name)}\s*$",
rf"^\s*#\[.*\]\s*\n\s*{re.escape(item_name)}\b",
]
else: # property
patterns = [rf"pub\s+{re.escape(item_name)}\s*:"]
for search_path in search_paths:
for rust_file in search_path.rglob("*.rs"):
try:
content = rust_file.read_text()
lines = content.split("\n")
if kind == "type":
for pattern in patterns:
for i, line in enumerate(lines):
if re.search(pattern, line):
return str(rust_file), i + 1
elif kind in ("variant", "variant_title", "property"):
parent_pattern = rf"pub\s+(?:enum|struct)\s+{type_name}\b"
in_type = False
brace_count = 0
found_open_brace = False
for i, line in enumerate(lines):
if re.search(parent_pattern, line):
in_type = True
brace_count = 0
found_open_brace = False
if in_type:
if "{" in line:
found_open_brace = True
brace_count += line.count("{") - line.count("}")
for pattern in patterns:
if re.search(pattern, line):
return str(rust_file), i + 1
if found_open_brace and brace_count <= 0:
in_type = False
except Exception:
continue
return None, None
def _get_variant_identifier(variant: dict) -> str:
"""Extract a meaningful identifier for a variant.
Tries to find the best identifier by checking:
1. A top-level const value (e.g., {"const": "Linear"})
2. A property with a const value (e.g., {"kind": {"const": "Bar"}})
3. The first required property name
4. The type field
5. Falls back to "unknown"
"""
# Check for top-level const value (simple enum variant)
if "const" in variant:
return str(variant["const"])
properties = variant.get("properties", {})
# Check for a property with a const value (common pattern for tagged enums)
for prop_name, prop_def in properties.items():
if isinstance(prop_def, dict) and "const" in prop_def:
return str(prop_def["const"])
# Fall back to first required property name
required = variant.get("required", [])
if required:
return str(required[0])
# Fall back to type
if "type" in variant:
return str(variant["type"])
return "unknown"
def check_type_description(type_name: str, type_def: dict) -> list[MissingDoc]:
"""Check if a type definition has proper documentation."""
missing = []
has_top_description = "description" in type_def
# Always check for top-level type description first
# (except for types that are purely references or have special handling)
needs_type_description = True
# Check oneOf variants (tagged enums with variant descriptions)
if "oneOf" in type_def:
# oneOf types should have a top-level description
if not has_top_description:
missing.append(MissingDoc(type_name, "type", None, None, None))
for variant in type_def["oneOf"]:
# Case 1: Simple const variant (e.g., {"const": "Swap", "description": "..."})
variant_name = variant.get("const") or variant.get("title")
if variant_name and "description" not in variant:
missing.append(
MissingDoc(type_name, "variant", str(variant_name), None, None)
)
# Case 2: String enum inside oneOf (e.g., {"type": "string", "enum": [...]})
# These variants don't have individual descriptions in the schema
if "enum" in variant and variant.get("type") == "string":
for enum_variant in variant["enum"]:
missing.append(
MissingDoc(type_name, "variant", str(enum_variant), None, None)
)
# Case 3: Object variant with properties (e.g., CubicBezier)
if "properties" in variant and "description" not in variant:
for prop_name in variant.get("required", []):
missing.append(
MissingDoc(type_name, "variant", str(prop_name), None, None)
)
# Case 4: Object variant missing title (needed for schema UI display)
# Object variants should have a title or const for proper display in editors
if (
"properties" in variant
and "title" not in variant
and "const" not in variant
):
# Try to find a good identifier for the variant (for display only)
variant_id = _get_variant_identifier(variant)
missing.append(
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
)
# Check anyOf variants - check each variant individually
elif "anyOf" in type_def:
# anyOf types should have a top-level description
if not has_top_description:
missing.append(MissingDoc(type_name, "type", None, None, None))
# Check each variant for description (skip pure $ref and null types)
for variant in type_def["anyOf"]:
# Skip null type variants (used for Option<T>)
if variant.get("type") == "null":
continue
# Skip pure $ref variants (the referenced type is checked separately)
if "$ref" in variant and len(variant) == 1:
continue
# Skip $ref variants that have a description
if "$ref" in variant and "description" in variant:
continue
# Variant with $ref but no description
if "$ref" in variant and "description" not in variant:
# Extract the type name from the $ref
ref_name = variant["$ref"].split("/")[-1]
missing.append(MissingDoc(type_name, "variant", ref_name, None, None))
# Non-ref variant without description
elif "description" not in variant and "$ref" not in variant:
# Try to identify the variant by its type or const
variant_id = variant.get("const") or variant.get("type") or "unknown"
missing.append(
MissingDoc(type_name, "variant", str(variant_id), None, None)
)
# Check for missing title on object variants in anyOf
if (
"properties" in variant
and "title" not in variant
and "const" not in variant
):
variant_id = _get_variant_identifier(variant)
missing.append(
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
)
# Check simple string enums (no oneOf means no variant descriptions possible in schema)
elif "enum" in type_def:
if not has_top_description:
missing.append(MissingDoc(type_name, "type", None, None, None))
# Each enum variant needs a docstring - these can't have descriptions in simple enum format
for variant in type_def["enum"]:
missing.append(MissingDoc(type_name, "variant", str(variant), None, None))
# Check struct properties
elif "properties" in type_def:
# Structs should always have a top-level description
if not has_top_description:
missing.append(MissingDoc(type_name, "type", None, None, None))
for prop_name, prop_def in type_def["properties"].items():
if "description" not in prop_def:
missing.append(MissingDoc(type_name, "property", prop_name, None, None))
# Simple type without description (like PathBuf, Hex)
elif not has_top_description:
# Only flag if it has a concrete type (not just a $ref)
if type_def.get("type") is not None:
missing.append(MissingDoc(type_name, "type", None, None, None))
return missing
def check_top_level_properties(schema: dict, root_type_name: str) -> list[MissingDoc]:
"""Check top-level schema properties for missing descriptions."""
missing = []
properties = schema.get("properties", {})
for prop_name, prop_def in properties.items():
if "description" not in prop_def:
missing.append(
MissingDoc(root_type_name, "property", prop_name, None, None)
)
return missing
def check_schema(
schema_path: Path,
search_paths: list[Path],
project_root: Path,
display_name: str,
) -> tuple[list[MissingDoc], int]:
"""Check a single schema file and return missing docs and exit code."""
if not schema_path.exists():
print(f"Error: {schema_path.name} not found at {schema_path}")
return [], 1
with open(schema_path) as f:
schema = json.load(f)
all_missing: list[MissingDoc] = []
# Check top-level schema properties
root_type_name = schema.get("title", "Root")
all_missing.extend(check_top_level_properties(schema, root_type_name))
# Check all type definitions
for type_name, type_def in sorted(schema.get("$defs", {}).items()):
# Skip PerAnimationPrefixConfig2/3 as they're generated variants
if (
type_name.startswith("PerAnimationPrefixConfig")
and type_name != "PerAnimationPrefixConfig"
):
continue
all_missing.extend(check_type_description(type_name, type_def))
# Find Rust source locations
print(f"Scanning Rust source files for {display_name}...", file=sys.stderr)
for doc in all_missing:
doc.rust_file, doc.rust_line = find_rust_definition(
doc.type_name, doc.item_name, doc.kind, search_paths
)
if doc.rust_file:
try:
doc.rust_file = str(Path(doc.rust_file).relative_to(project_root))
except ValueError:
pass
return all_missing, 0
def print_results(all_missing: list[MissingDoc], display_name: str) -> None:
"""Print the results for a schema check."""
# Group by file
by_file: dict[str, list[MissingDoc]] = {}
external: list[MissingDoc] = []
for doc in all_missing:
if doc.rust_file:
by_file.setdefault(doc.rust_file, []).append(doc)
else:
external.append(doc)
# Print summary
print("\n" + "=" * 70)
print(f"MISSING DOCSTRINGS IN SCHEMA ({display_name})")
print("=" * 70)
type_count = sum(1 for d in all_missing if d.kind == "type")
variant_count = sum(1 for d in all_missing if d.kind == "variant")
variant_title_count = sum(1 for d in all_missing if d.kind == "variant_title")
prop_count = sum(1 for d in all_missing if d.kind == "property")
print(f"\nTotal: {len(all_missing)} missing docstrings/titles")
print(f" - {type_count} types")
print(f" - {variant_count} variants")
print(f" - {variant_title_count} variant titles")
print(f" - {prop_count} properties")
# Print by file
for rust_file in sorted(by_file.keys()):
docs = sorted(by_file[rust_file], key=lambda d: d.rust_line or 0)
print(f"\n{rust_file}:")
print("-" * len(rust_file))
for doc in docs:
print(f" {doc}")
# Print external items (types not found in source)
if external:
print(f"\nExternal/Unknown location:")
print("-" * 25)
for doc in external:
print(f" {doc}")
print("\n" + "=" * 70)
def main():
project_root = Path.cwd()
# Define schemas to check with their respective search paths
schemas = [
SchemaConfig(
schema_file="schema.json",
search_paths=["komorebi/src", "komorebi-themes/src"],
display_name="komorebi",
),
SchemaConfig(
schema_file="schema.bar.json",
search_paths=["komorebi-bar/src", "komorebi-themes/src"],
display_name="komorebi-bar",
),
]
total_missing = 0
has_errors = False
for schema_config in schemas:
schema_path = project_root / schema_config.schema_file
search_paths = [
project_root / p
for p in schema_config.search_paths
if (project_root / p).exists()
]
missing, error_code = check_schema(
schema_path,
search_paths,
project_root,
schema_config.display_name,
)
if error_code != 0:
has_errors = True
continue
print_results(missing, schema_config.display_name)
total_missing += len(missing)
# Print combined summary
if len(schemas) > 1:
print("\n" + "=" * 70)
print("COMBINED SUMMARY")
print("=" * 70)
print(f"Total missing docstrings across all schemas: {total_missing}")
print("=" * 70)
if has_errors:
return 1
return 1 if total_missing > 0 else 0
if __name__ == "__main__":
sys.exit(main())

123
deny.toml Normal file
View File

@@ -0,0 +1,123 @@
[graph]
targets = [
"x86_64-pc-windows-msvc",
"i686-pc-windows-msvc",
"aarch64-pc-windows-msvc",
]
all-features = false
no-default-features = false
[output]
feature-depth = 1
[advisories]
ignore = [
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" },
]
[licenses]
allow = [
"0BSD",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"Artistic-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"ISC",
"MIT",
"MIT-0",
"MPL-2.0",
"OFL-1.1",
"Ubuntu-font-1.0",
"Unicode-3.0",
"Zlib",
"LicenseRef-Komorebi-2.0",
]
confidence-threshold = 0.8
[[licenses.clarify]]
crate = "komorebi"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-client"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebic"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebic-no-console"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-themes"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-gui"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-bar"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-shortcuts"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "whkd-core"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "whkd-parser"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "base16-egui-themes"
expression = "MIT"
license-files = []
[[licenses.clarify]]
crate = "win32-display-data"
expression = "0BSD"
license-files = []
[bans]
multiple-versions = "allow"
wildcards = "allow"
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = [
"https://github.com/LGUG2Z/base16-egui-themes",
"https://github.com/LGUG2Z/windows-icons",
"https://github.com/LGUG2Z/win32-display-data",
"https://github.com/LGUG2Z/flavours",
"https://github.com/LGUG2Z/base16_color_scheme",
"https://github.com/LGUG2Z/whkd",
"https://github.com/LGUG2Z/catppuccin-egui",
"https://github.com/amPerl/egui-phosphor",
]

987
dependencies.json Normal file
View File

@@ -0,0 +1,987 @@
{
"licenses": [
[
"0BSD",
[
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"adler2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"win32-display-data 0.1.0 git+https://github.com/LGUG2Z/win32-display-data?rev=8c42d8db257d30fe95bc98c2e5cd8f75da861021"
]
],
[
"Apache-2.0",
[
"ab_glyph 0.2.32 registry+https://github.com/rust-lang/crates.io-index",
"ab_glyph_rasterizer 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
"accesskit 0.21.1 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_consumer 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_windows 0.29.2 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_winit 0.29.2 registry+https://github.com/rust-lang/crates.io-index",
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"adler2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"ahash 0.8.12 registry+https://github.com/rust-lang/crates.io-index",
"aligned 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"allocator-api2 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
"anstream 0.6.21 registry+https://github.com/rust-lang/crates.io-index",
"anstyle 1.0.13 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-parse 0.2.7 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-query 1.1.5 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-wincon 3.0.11 registry+https://github.com/rust-lang/crates.io-index",
"anyhow 1.0.100 registry+https://github.com/rust-lang/crates.io-index",
"approx 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"arboard 3.6.1 registry+https://github.com/rust-lang/crates.io-index",
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
"as-slice 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
"autocfg 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"backtrace 0.3.76 registry+https://github.com/rust-lang/crates.io-index",
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"base16_color_scheme 0.3.2 git+https://github.com/LGUG2Z/base16_color_scheme",
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
"beef 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"bit_field 0.10.3 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 2.10.0 registry+https://github.com/rust-lang/crates.io-index",
"bitstream-io 4.9.0 registry+https://github.com/rust-lang/crates.io-index",
"block-buffer 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck 1.24.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck_derive 1.10.2 registry+https://github.com/rust-lang/crates.io-index",
"cc 1.2.51 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"chrono 0.4.42 registry+https://github.com/rust-lang/crates.io-index",
"chrono-tz 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
"clap 4.5.54 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.5.54 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.5.49 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
"color-eyre 0.6.5 registry+https://github.com/rust-lang/crates.io-index",
"color-spantrace 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
"colorchoice 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"core2 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"cpufeatures 0.2.17 registry+https://github.com/rust-lang/crates.io-index",
"crc32fast 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
"crypto-common 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
"ctrlc 3.5.1 registry+https://github.com/rust-lang/crates.io-index",
"cursor-icon 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"curve25519-dalek-derive 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"deflate 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"deranged 0.5.5 registry+https://github.com/rust-lang/crates.io-index",
"digest 0.10.7 registry+https://github.com/rust-lang/crates.io-index",
"dirs 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
"dirs 4.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"document-features 0.2.12 registry+https://github.com/rust-lang/crates.io-index",
"dpi 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"dyn-clone 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
"ecolor 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"ed25519 2.2.3 registry+https://github.com/rust-lang/crates.io-index",
"eframe 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui-phosphor 0.10.0 git+https://github.com/amPerl/egui-phosphor?rev=d13688738478ecd12b426e3e74c59d6577a85b59",
"egui-winit 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui_extras 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui_glow 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"either 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
"emath 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"epaint 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"epaint_default_fonts 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"filetime 0.2.26 registry+https://github.com/rust-lang/crates.io-index",
"find-msvc-tools 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"flate2 1.1.5 registry+https://github.com/rust-lang/crates.io-index",
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
"form_urlencoded 1.2.2 registry+https://github.com/rust-lang/crates.io-index",
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.11.4 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.14.1 registry+https://github.com/rust-lang/crates.io-index",
"git2 0.20.3 registry+https://github.com/rust-lang/crates.io-index",
"gl_generator 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
"glob 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
"glutin 0.32.3 registry+https://github.com/rust-lang/crates.io-index",
"glutin_egl_sys 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"glutin_wgl_sys 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"half 2.7.1 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.15.5 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.16.1 registry+https://github.com/rust-lang/crates.io-index",
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"http 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
"iana-time-zone 0.1.64 registry+https://github.com/rust-lang/crates.io-index",
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"idna 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"idna_adapter 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"image 0.25.9 registry+https://github.com/rust-lang/crates.io-index",
"image-webp 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"imgref 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
"indenter 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 2.13.0 registry+https://github.com/rust-lang/crates.io-index",
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
"iri-string 0.7.10 registry+https://github.com/rust-lang/crates.io-index",
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"is_terminal_polyfill 1.70.2 registry+https://github.com/rust-lang/crates.io-index",
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
"itoa 1.0.17 registry+https://github.com/rust-lang/crates.io-index",
"jobserver 0.1.34 registry+https://github.com/rust-lang/crates.io-index",
"jpeg-decoder 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
"khronos_api 3.1.0 registry+https://github.com/rust-lang/crates.io-index",
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"libc 0.2.180 registry+https://github.com/rust-lang/crates.io-index",
"libgit2-sys 0.18.3+1.9.2 registry+https://github.com/rust-lang/crates.io-index",
"libz-sys 1.1.23 registry+https://github.com/rust-lang/crates.io-index",
"linked-hash-map 0.5.6 registry+https://github.com/rust-lang/crates.io-index",
"litrs 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"lock_api 0.4.14 registry+https://github.com/rust-lang/crates.io-index",
"log 0.4.29 registry+https://github.com/rust-lang/crates.io-index",
"logos 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-codegen 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-derive 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"miette 7.6.0 registry+https://github.com/rust-lang/crates.io-index",
"miette-derive 7.6.0 registry+https://github.com/rust-lang/crates.io-index",
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
"miow 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"moxcms 0.7.11 registry+https://github.com/rust-lang/crates.io-index",
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
"nohash-hasher 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"ntapi 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
"object 0.32.2 registry+https://github.com/rust-lang/crates.io-index",
"once_cell 1.21.3 registry+https://github.com/rust-lang/crates.io-index",
"once_cell_polyfill 1.70.2 registry+https://github.com/rust-lang/crates.io-index",
"owned_ttf_parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
"palette 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot 0.12.5 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot_core 0.9.12 registry+https://github.com/rust-lang/crates.io-index",
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
"pastey 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"percent-encoding 2.3.2 registry+https://github.com/rust-lang/crates.io-index",
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"png 0.16.8 registry+https://github.com/rust-lang/crates.io-index",
"png 0.18.0 registry+https://github.com/rust-lang/crates.io-index",
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"ppv-lite86 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro2 1.0.105 registry+https://github.com/rust-lang/crates.io-index",
"profiling 1.0.17 registry+https://github.com/rust-lang/crates.io-index",
"profiling-procmacros 1.0.17 registry+https://github.com/rust-lang/crates.io-index",
"psm 0.1.28 registry+https://github.com/rust-lang/crates.io-index",
"pxfm 0.1.27 registry+https://github.com/rust-lang/crates.io-index",
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"quote 1.0.43 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.9.2 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.9.3 registry+https://github.com/rust-lang/crates.io-index",
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
"rayon 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
"rayon-core 1.13.0 registry+https://github.com/rust-lang/crates.io-index",
"ref-cast 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
"ref-cast-impl 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
"regex 1.12.2 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.4.13 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
"reqwest 0.12.28 registry+https://github.com/rust-lang/crates.io-index",
"roxmltree 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
"rustc-demangle 0.1.26 registry+https://github.com/rust-lang/crates.io-index",
"rustc_version 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"rustls-pki-types 1.13.2 registry+https://github.com/rust-lang/crates.io-index",
"ryu 1.0.22 registry+https://github.com/rust-lang/crates.io-index",
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"semver 1.0.27 registry+https://github.com/rust-lang/crates.io-index",
"serde 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
"serde_core 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_json 1.0.149 registry+https://github.com/rust-lang/crates.io-index",
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"serde_with 3.16.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_with_macros 3.16.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
"sha2 0.10.9 registry+https://github.com/rust-lang/crates.io-index",
"shadow-rs 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"shell-words 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
"shellexpand 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
"signature 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"smallvec 1.15.1 registry+https://github.com/rust-lang/crates.io-index",
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"socket2 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"stable_deref_trait 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"stacker 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"supports-color 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
"supports-hyperlinks 3.2.0 registry+https://github.com/rust-lang/crates.io-index",
"supports-unicode 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
"syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index",
"syn 2.0.114 registry+https://github.com/rust-lang/crates.io-index",
"sync_wrapper 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"tempfile 3.24.0 registry+https://github.com/rust-lang/crates.io-index",
"terminal_size 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"thiserror 2.0.17 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 2.0.17 registry+https://github.com/rust-lang/crates.io-index",
"thread_local 1.1.9 registry+https://github.com/rust-lang/crates.io-index",
"time 0.3.44 registry+https://github.com/rust-lang/crates.io-index",
"time-core 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index",
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
"typenum 1.19.0 registry+https://github.com/rust-lang/crates.io-index",
"tz-rs 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"tzdb 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
"tzdb_data 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
"unicase 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-ident 1.0.22 registry+https://github.com/rust-lang/crates.io-index",
"unicode-linebreak 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
"unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
"uom 0.37.0 registry+https://github.com/rust-lang/crates.io-index",
"url 2.5.8 registry+https://github.com/rust-lang/crates.io-index",
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
"version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"webbrowser 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"weezl 0.1.12 registry+https://github.com/rust-lang/crates.io-index",
"win-msgbox 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.61.3 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.62.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.61.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.62.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.60.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.59.3 registry+https://github.com/rust-lang/crates.io-index",
"windows-link 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"windows-link 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-registry 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.60.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.61.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.53.5 registry+https://github.com/rust-lang/crates.io-index",
"windows-threading 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-threading 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.53.1 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.53.1 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.53.1 registry+https://github.com/rust-lang/crates.io-index",
"winit 0.30.12 registry+https://github.com/rust-lang/crates.io-index",
"wmi 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
"yaml-rust 0.4.5 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.33 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.33 registry+https://github.com/rust-lang/crates.io-index",
"zeroize 1.8.2 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.4.21 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.5.8 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Apache-2.0 WITH LLVM-exception",
[
"ar_archive_writer 0.2.0 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Artistic-2.0",
[
"file-id 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"notify-debouncer-full 0.1.0 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"BSD-2-Clause",
[
"av1-grain 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"rav1e 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
"v_frame 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.33 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.33 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"BSD-3-Clause",
[
"alloc-no-stdlib 2.0.4 registry+https://github.com/rust-lang/crates.io-index",
"alloc-stdlib 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"avif-serialize 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"brotli 8.0.2 registry+https://github.com/rust-lang/crates.io-index",
"brotli-decompressor 5.0.0 registry+https://github.com/rust-lang/crates.io-index",
"curve25519-dalek 4.1.3 registry+https://github.com/rust-lang/crates.io-index",
"ed25519-dalek 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
"exr 1.74.0 registry+https://github.com/rust-lang/crates.io-index",
"lebe 0.5.3 registry+https://github.com/rust-lang/crates.io-index",
"moxcms 0.7.11 registry+https://github.com/rust-lang/crates.io-index",
"pxfm 0.1.27 registry+https://github.com/rust-lang/crates.io-index",
"ravif 0.12.0 registry+https://github.com/rust-lang/crates.io-index",
"subtle 2.6.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"BSL-1.0",
[
"clipboard-win 5.4.1 registry+https://github.com/rust-lang/crates.io-index",
"error-code 3.3.2 registry+https://github.com/rust-lang/crates.io-index",
"ryu 1.0.22 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"CC0-1.0",
[
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"file-id 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"imgref 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
"notify 6.1.1 registry+https://github.com/rust-lang/crates.io-index",
"notify-debouncer-full 0.1.0 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"ISC",
[
"is_ci 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"libloading 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
"starship-battery 0.10.3 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"MIT",
[
"accesskit 0.21.1 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_consumer 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_windows 0.29.2 registry+https://github.com/rust-lang/crates.io-index",
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"adler2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"ahash 0.8.12 registry+https://github.com/rust-lang/crates.io-index",
"aho-corasick 1.1.4 registry+https://github.com/rust-lang/crates.io-index",
"aligned 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"aligned-vec 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
"allocator-api2 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
"anstream 0.6.21 registry+https://github.com/rust-lang/crates.io-index",
"anstyle 1.0.13 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-parse 0.2.7 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-query 1.1.5 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-wincon 3.0.11 registry+https://github.com/rust-lang/crates.io-index",
"anyhow 1.0.100 registry+https://github.com/rust-lang/crates.io-index",
"arboard 3.6.1 registry+https://github.com/rust-lang/crates.io-index",
"arg_enum_proc_macro 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
"as-slice 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
"autocfg 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"av-scenechange 0.14.1 registry+https://github.com/rust-lang/crates.io-index",
"backtrace 0.3.76 registry+https://github.com/rust-lang/crates.io-index",
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"base16_color_scheme 0.3.2 git+https://github.com/LGUG2Z/base16_color_scheme",
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
"beef 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"bit_field 0.10.3 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 2.10.0 registry+https://github.com/rust-lang/crates.io-index",
"bitstream-io 4.9.0 registry+https://github.com/rust-lang/crates.io-index",
"block-buffer 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
"brotli 8.0.2 registry+https://github.com/rust-lang/crates.io-index",
"brotli-decompressor 5.0.0 registry+https://github.com/rust-lang/crates.io-index",
"built 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck 1.24.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck_derive 1.10.2 registry+https://github.com/rust-lang/crates.io-index",
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"byteorder-lite 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"bytes 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
"calm_io 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"calmio_filters 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"catppuccin-egui 5.6.0 git+https://github.com/LGUG2Z/catppuccin-egui?rev=b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a",
"cc 1.2.51 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"cfg_aliases 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"chrono 0.4.42 registry+https://github.com/rust-lang/crates.io-index",
"chrono-tz 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
"chumsky 0.9.3 registry+https://github.com/rust-lang/crates.io-index",
"clap 4.5.54 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.5.54 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.5.49 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
"color-eyre 0.6.5 registry+https://github.com/rust-lang/crates.io-index",
"color-spantrace 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
"color-thief 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"color_quant 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"colorchoice 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"core2 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"cpufeatures 0.2.17 registry+https://github.com/rust-lang/crates.io-index",
"crc32fast 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
"crypto-common 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
"ctrlc 3.5.1 registry+https://github.com/rust-lang/crates.io-index",
"cursor-icon 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"curve25519-dalek-derive 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"darling 0.21.3 registry+https://github.com/rust-lang/crates.io-index",
"darling_core 0.21.3 registry+https://github.com/rust-lang/crates.io-index",
"darling_macro 0.21.3 registry+https://github.com/rust-lang/crates.io-index",
"deflate 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"deranged 0.5.5 registry+https://github.com/rust-lang/crates.io-index",
"digest 0.10.7 registry+https://github.com/rust-lang/crates.io-index",
"dirs 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
"dirs 4.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"document-features 0.2.12 registry+https://github.com/rust-lang/crates.io-index",
"dpi 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
"dyn-clone 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
"ecolor 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"ed25519 2.2.3 registry+https://github.com/rust-lang/crates.io-index",
"eframe 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui-phosphor 0.10.0 git+https://github.com/amPerl/egui-phosphor?rev=d13688738478ecd12b426e3e74c59d6577a85b59",
"egui-winit 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui_extras 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"egui_glow 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"either 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
"emath 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"epaint 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"epaint_default_fonts 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
"equator 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"equator-macro 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
"fax 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
"fax_derive 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"filetime 0.2.26 registry+https://github.com/rust-lang/crates.io-index",
"find-msvc-tools 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"flate2 1.1.5 registry+https://github.com/rust-lang/crates.io-index",
"flavours 0.7.2 git+https://github.com/LGUG2Z/flavours",
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
"font-loader 0.11.0 registry+https://github.com/rust-lang/crates.io-index",
"form_urlencoded 1.2.2 registry+https://github.com/rust-lang/crates.io-index",
"fs-tail 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"generic-array 0.14.7 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.11.4 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.14.1 registry+https://github.com/rust-lang/crates.io-index",
"git2 0.20.3 registry+https://github.com/rust-lang/crates.io-index",
"glob 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
"glutin-winit 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"h2 0.4.13 registry+https://github.com/rust-lang/crates.io-index",
"half 2.7.1 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.15.5 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.16.1 registry+https://github.com/rust-lang/crates.io-index",
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"http 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
"http-body 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"http-body-util 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
"hyper 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
"hyper-util 0.1.19 registry+https://github.com/rust-lang/crates.io-index",
"iana-time-zone 0.1.64 registry+https://github.com/rust-lang/crates.io-index",
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"idna 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"idna_adapter 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"image 0.23.14 registry+https://github.com/rust-lang/crates.io-index",
"image 0.25.9 registry+https://github.com/rust-lang/crates.io-index",
"image-webp 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"indenter 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 2.13.0 registry+https://github.com/rust-lang/crates.io-index",
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
"iri-string 0.7.10 registry+https://github.com/rust-lang/crates.io-index",
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"is_terminal_polyfill 1.70.2 registry+https://github.com/rust-lang/crates.io-index",
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
"itoa 1.0.17 registry+https://github.com/rust-lang/crates.io-index",
"jobserver 0.1.34 registry+https://github.com/rust-lang/crates.io-index",
"jpeg-decoder 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"libc 0.2.180 registry+https://github.com/rust-lang/crates.io-index",
"libgit2-sys 0.18.3+1.9.2 registry+https://github.com/rust-lang/crates.io-index",
"libz-sys 1.1.23 registry+https://github.com/rust-lang/crates.io-index",
"linked-hash-map 0.5.6 registry+https://github.com/rust-lang/crates.io-index",
"litrs 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"lock_api 0.4.14 registry+https://github.com/rust-lang/crates.io-index",
"log 0.4.29 registry+https://github.com/rust-lang/crates.io-index",
"logos 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-codegen 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-derive 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"loop9 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
"mac-addr 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
"matchers 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"maybe-rayon 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"memchr 2.7.6 registry+https://github.com/rust-lang/crates.io-index",
"memoffset 0.9.1 registry+https://github.com/rust-lang/crates.io-index",
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
"mime_guess2 2.3.1 registry+https://github.com/rust-lang/crates.io-index",
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
"mio 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
"miow 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"nanoid 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
"netdev 0.40.0 registry+https://github.com/rust-lang/crates.io-index",
"new_debug_unreachable 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"nohash-hasher 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"nom 7.1.3 registry+https://github.com/rust-lang/crates.io-index",
"nom 8.0.0 registry+https://github.com/rust-lang/crates.io-index",
"noop_proc_macro 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
"ntapi 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"nu-ansi-term 0.50.3 registry+https://github.com/rust-lang/crates.io-index",
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
"object 0.32.2 registry+https://github.com/rust-lang/crates.io-index",
"once_cell 1.21.3 registry+https://github.com/rust-lang/crates.io-index",
"once_cell_polyfill 1.70.2 registry+https://github.com/rust-lang/crates.io-index",
"open 5.3.3 registry+https://github.com/rust-lang/crates.io-index",
"os_info 3.14.0 registry+https://github.com/rust-lang/crates.io-index",
"owo-colors 4.2.3 registry+https://github.com/rust-lang/crates.io-index",
"palette 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot 0.12.5 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot_core 0.9.12 registry+https://github.com/rust-lang/crates.io-index",
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
"pastey 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"percent-encoding 2.3.2 registry+https://github.com/rust-lang/crates.io-index",
"phf 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
"phf 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
"phf 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"phf_codegen 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"phf_generator 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"phf_shared 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
"phf_shared 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
"phf_shared 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"png 0.16.8 registry+https://github.com/rust-lang/crates.io-index",
"png 0.18.0 registry+https://github.com/rust-lang/crates.io-index",
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"powershell_script 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"ppv-lite86 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro2 1.0.105 registry+https://github.com/rust-lang/crates.io-index",
"profiling 1.0.17 registry+https://github.com/rust-lang/crates.io-index",
"profiling-procmacros 1.0.17 registry+https://github.com/rust-lang/crates.io-index",
"psm 0.1.28 registry+https://github.com/rust-lang/crates.io-index",
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"quote 1.0.43 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.9.2 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.9.3 registry+https://github.com/rust-lang/crates.io-index",
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"random_word 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
"rayon 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
"rayon-core 1.13.0 registry+https://github.com/rust-lang/crates.io-index",
"ref-cast 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
"ref-cast-impl 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
"regex 1.12.2 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.4.13 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
"reqwest 0.12.28 registry+https://github.com/rust-lang/crates.io-index",
"rgb 0.8.52 registry+https://github.com/rust-lang/crates.io-index",
"roxmltree 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
"rustc-demangle 0.1.26 registry+https://github.com/rust-lang/crates.io-index",
"rustc_version 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"rustls-pki-types 1.13.2 registry+https://github.com/rust-lang/crates.io-index",
"same-file 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"schannel 0.1.28 registry+https://github.com/rust-lang/crates.io-index",
"schemars 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"schemars_derive 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"scoped_threadpool 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"semver 1.0.27 registry+https://github.com/rust-lang/crates.io-index",
"serde 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
"serde_core 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_json 1.0.149 registry+https://github.com/rust-lang/crates.io-index",
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"serde_with 3.16.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_with_macros 3.16.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
"sha2 0.10.9 registry+https://github.com/rust-lang/crates.io-index",
"shadow-rs 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"sharded-slab 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
"shell-words 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
"shellexpand 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
"signature 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
"simd-adler32 0.3.8 registry+https://github.com/rust-lang/crates.io-index",
"simd_helpers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"slab 0.4.11 registry+https://github.com/rust-lang/crates.io-index",
"smallvec 1.15.1 registry+https://github.com/rust-lang/crates.io-index",
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"socket2 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"stable_deref_trait 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"stacker 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"strsim 0.11.1 registry+https://github.com/rust-lang/crates.io-index",
"strum 0.27.2 registry+https://github.com/rust-lang/crates.io-index",
"strum_macros 0.27.2 registry+https://github.com/rust-lang/crates.io-index",
"syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index",
"syn 2.0.114 registry+https://github.com/rust-lang/crates.io-index",
"synstructure 0.13.2 registry+https://github.com/rust-lang/crates.io-index",
"sysinfo 0.33.1 registry+https://github.com/rust-lang/crates.io-index",
"sysinfo 0.37.2 registry+https://github.com/rust-lang/crates.io-index",
"tempfile 3.24.0 registry+https://github.com/rust-lang/crates.io-index",
"terminal_size 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"textwrap 0.16.2 registry+https://github.com/rust-lang/crates.io-index",
"thiserror 2.0.17 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 2.0.17 registry+https://github.com/rust-lang/crates.io-index",
"thread_local 1.1.9 registry+https://github.com/rust-lang/crates.io-index",
"tiff 0.10.3 registry+https://github.com/rust-lang/crates.io-index",
"tiff 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"time 0.3.44 registry+https://github.com/rust-lang/crates.io-index",
"time-core 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"tokio 1.49.0 registry+https://github.com/rust-lang/crates.io-index",
"tokio-native-tls 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"tokio-util 0.7.18 registry+https://github.com/rust-lang/crates.io-index",
"toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index",
"tower 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"tower-http 0.6.8 registry+https://github.com/rust-lang/crates.io-index",
"tower-layer 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"tower-service 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"tracing 0.1.44 registry+https://github.com/rust-lang/crates.io-index",
"tracing-appender 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"tracing-attributes 0.1.31 registry+https://github.com/rust-lang/crates.io-index",
"tracing-core 0.1.36 registry+https://github.com/rust-lang/crates.io-index",
"tracing-error 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"tracing-log 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"tracing-subscriber 0.3.22 registry+https://github.com/rust-lang/crates.io-index",
"try-lock 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
"typenum 1.19.0 registry+https://github.com/rust-lang/crates.io-index",
"tz-rs 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"tzdb_data 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
"uds_windows 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"unicase 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-ident 1.0.22 registry+https://github.com/rust-lang/crates.io-index",
"unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
"unsafe-libyaml 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
"uom 0.37.0 registry+https://github.com/rust-lang/crates.io-index",
"url 2.5.8 registry+https://github.com/rust-lang/crates.io-index",
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
"version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
"walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index",
"want 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"webbrowser 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"weezl 0.1.12 registry+https://github.com/rust-lang/crates.io-index",
"which 8.0.0 registry+https://github.com/rust-lang/crates.io-index",
"win-msgbox 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
"winapi-util 0.1.11 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.61.3 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.62.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.61.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.62.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-icons 0.1.0 git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354",
"windows-icons 0.1.0 git+https://github.com/LGUG2Z/windows-icons?rev=d67cc9920aa9b4883393e411fb4fa2ddd4c498b5",
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.60.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.59.3 registry+https://github.com/rust-lang/crates.io-index",
"windows-link 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"windows-link 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-registry 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.60.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.61.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.53.5 registry+https://github.com/rust-lang/crates.io-index",
"windows-threading 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-threading 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.53.1 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.53.1 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.53.1 registry+https://github.com/rust-lang/crates.io-index",
"winput 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"winreg 0.55.0 registry+https://github.com/rust-lang/crates.io-index",
"winsafe 0.0.19 registry+https://github.com/rust-lang/crates.io-index",
"wmi 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
"xml-rs 0.8.28 registry+https://github.com/rust-lang/crates.io-index",
"y4m 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"yaml-rust 0.4.5 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.33 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.33 registry+https://github.com/rust-lang/crates.io-index",
"zeroize 1.8.2 registry+https://github.com/rust-lang/crates.io-index",
"zmij 1.0.12 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.4.21 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.5.8 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"MIT-0",
[
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"tzdb_data 0.2.3 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"MPL-2.0",
[
"option-ext 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"ramhorns 1.0.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"OFL-1.1",
[
"epaint_default_fonts 0.33.3 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Ubuntu-font-1.0",
[
"epaint_default_fonts 0.33.3 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Unicode-3.0",
[
"icu_collections 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_locale_core 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_normalizer 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_normalizer_data 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_properties 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
"icu_properties_data 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
"icu_provider 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
"litemap 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
"potential_utf 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
"tinystr 0.8.2 registry+https://github.com/rust-lang/crates.io-index",
"unicode-ident 1.0.22 registry+https://github.com/rust-lang/crates.io-index",
"writeable 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
"yoke 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
"yoke-derive 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
"zerofrom 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"zerofrom-derive 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"zerotrie 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
"zerovec 0.11.5 registry+https://github.com/rust-lang/crates.io-index",
"zerovec-derive 0.11.2 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Unlicense",
[
"aho-corasick 1.1.4 registry+https://github.com/rust-lang/crates.io-index",
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"byteorder-lite 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"memchr 2.7.6 registry+https://github.com/rust-lang/crates.io-index",
"same-file 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index",
"winapi-util 0.1.11 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Zlib",
[
"adler32 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck 1.24.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck_derive 1.10.2 registry+https://github.com/rust-lang/crates.io-index",
"const_format 0.2.35 registry+https://github.com/rust-lang/crates.io-index",
"const_format_proc_macros 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
"cursor-icon 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"foldhash 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.4.21 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.5.8 registry+https://github.com/rust-lang/crates.io-index"
]
]
]
}

View File

@@ -10,9 +10,8 @@ Options:
Desired ease function for animation
[default: linear]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo,
ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the style to. If not specified, sets global style

View File

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

View File

@@ -0,0 +1,12 @@
# cancel-preselect
```
Cancel a workspace preselect set by the preselect-direction command, if one exists
Usage: komorebic.exe cancel-preselect
Options:
-h, --help
Print help
```

View File

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

View File

@@ -0,0 +1,12 @@
# clear-session-float-rules
```
Clear all session float rules
Usage: komorebic.exe clear-session-float-rules
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,16 @@
# cycle-empty-workspace
```
Focus the next empty workspace in the given cycle direction (if one exists)
Usage: komorebic.exe cycle-empty-workspace <CYCLE_DIRECTION>
Arguments:
<CYCLE_DIRECTION>
[possible values: previous, next]
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# data-directory
```
Show the path to komorebi's data directory in %LOCALAPPDATA%
Usage: komorebic.exe data-directory
Options:
-h, --help
Print help
```

View File

@@ -12,9 +12,6 @@ Options:
--whkd
Enable autostart of whkd
--ahk
Enable autostart of ahk
--bar
Enable autostart of komorebi-bar

View File

@@ -9,9 +9,6 @@ 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

16
docs/cli/license.md Normal file
View File

@@ -0,0 +1,16 @@
# license
```
Specify an email associated with an Individual Commercial Use License
Usage: komorebic.exe license <EMAIL>
Arguments:
<EMAIL>
Email address associated with an Individual Commercial Use License
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# move-to-last-workspace
```
Move the focused window to the last focused monitor workspace
Usage: komorebic.exe move-to-last-workspace
Options:
-h, --help
Print help
```

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
# preselect-direction
```
Preselect the specified direction for the next window to be spawned on supported layouts
Usage: komorebic.exe preselect-direction <OPERATION_DIRECTION>
Arguments:
<OPERATION_DIRECTION>
[possible values: left, right, up, down]
Options:
-h, --help
Print help
```

12
docs/cli/promote-swap.md Normal file
View File

@@ -0,0 +1,12 @@
# promote-swap
```
Promote the focused window to the largest tile by swapping container indices with the largest tile
Usage: komorebic.exe promote-swap
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# promote
```
Promote the focused window to the top of the tree
Promote the focused window to the largest tile via container removal and re-insertion
Usage: komorebic.exe promote

View File

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

View File

@@ -0,0 +1,16 @@
# scrolling-layout-columns
```
Set the number of visible columns for the Scrolling layout on the focused workspace
Usage: komorebic.exe scrolling-layout-columns <COUNT>
Arguments:
<COUNT>
Desired number of visible columns
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# send-to-last-workspace
```
Send the focused window to the last focused monitor workspace
Usage: komorebic.exe send-to-last-workspace
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# session-float-rule
```
Add a rule to float the foreground window for the rest of this session
Usage: komorebic.exe session-float-rule
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# session-float-rules
```
Show all session float rules
Usage: komorebic.exe session-float-rules
Options:
-h, --help
Print help
```

View File

@@ -18,9 +18,6 @@ Options:
--whkd
Start whkd in a background process
--ahk
Start autohotkey configuration file
--bar
Start komorebi-bar in a background process

View File

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

12
docs/cli/toggle-lock.md Normal file
View File

@@ -0,0 +1,12 @@
# toggle-lock
```
Toggle a lock for the focused container, ensuring it will not be displaced by any new windows
Usage: komorebic.exe toggle-lock
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# toggle-pause
```
Toggle window tiling on the focused workspace
Toggle the paused state for all window tiling
Usage: komorebic.exe toggle-pause

View File

@@ -0,0 +1,12 @@
# toggle-shortcuts
```
Toggle the komorebi-shortcuts helper
Usage: komorebic.exe toggle-shortcuts
Options:
-h, --help
Print help
```

View File

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

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

View File

@@ -8,7 +8,7 @@ Usage: komorebic.exe window-hiding-behaviour <HIDING_BEHAVIOUR>
Arguments:
<HIDING_BEHAVIOUR>
Possible values:
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
- hide: END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
# workspace-work-area-offset
```
Set offsets for a workspace to exclude parts of the work area from tiling
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index (zero-indexed)
<LEFT>
Size of the left work area offset (set right to left * 2 to maintain right padding)
<TOP>
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
<RIGHT>
Size of the right work area offset
<BOTTOM>
Size of the bottom work area offset
Options:
-h, --help
Print help
```

View File

@@ -6,7 +6,10 @@ defined in the `komorebi.json` configuration file.
```json
{
"animation": {
"enabled": true
"enabled": true,
"duration": 250,
"fps": 60,
"style": "EaseOutSine"
}
}
```

View File

@@ -0,0 +1,419 @@
# Multi-Monitor Setup
You can set up komorebi to work with multiple monitors. To do so, first you start by setting up multiple monitor
configurations on your `komorebi.json` config file.
If you've used the [`komorebic quickstart`](../cli/quickstart.md) command you'll already have a `komorebi.json` config
file with one monitor config setup. Open this file and look for the `"monitors":` line, you should find something like
this:
```json
{
"monitors": [
{
"workspaces": [
{
"name": "I",
"layout": "BSP"
},
{
"name": "II",
"layout": "VerticalStack"
},
{
"name": "III",
"layout": "HorizontalStack"
},
{
"name": "IV",
"layout": "UltrawideVerticalStack"
},
{
"name": "V",
"layout": "Rows"
},
{
"name": "VI",
"layout": "Grid"
},
{
"name": "VII",
"layout": "RightMainVerticalStack"
}
]
}
]
}
```
For this example we will remove some workspaces to simplify the config so it is easier to look at, but feel free to
set up as many workspaces per monitor as you'd like. Here is the same configuration with only 3 workspaces.
```json
{
"monitors": [
{
"workspaces": [
{
"name": "I",
"layout": "BSP"
},
{
"name": "II",
"layout": "VerticalStack"
},
{
"name": "III",
"layout": "HorizontalStack"
}
]
}
]
}
```
Let's add another monitor:
```json
{
"monitors": [
// monitor 1, index 0
{
"workspaces": [
{
"name": "I",
"layout": "BSP"
},
{
"name": "II",
"layout": "VerticalStack"
},
{
"name": "III",
"layout": "HorizontalStack"
}
]
},
// monitor 2, index 1
{
"workspaces": [
{
"name": "1",
"layout": "BSP"
},
{
"name": "2",
"layout": "VerticalStack"
},
{
"name": "3",
"layout": "HorizontalStack"
}
]
}
]
}
```
Now have two monitor configurations. We have the first monitor configuration, which is index 0 (*usually
on programming languages the first item of a list starts with index 0*), this configuration has 3 workspaces with names
"I", "II" and "III". Then the 2nd monitor configuration, which is index 1, also has 3 workspaces with names "1", "2",
and "3" (you should always give unique names to your workspaces).
Now if you start komorebi with two monitors connected, the main monitor will use the configuration with index 0 and the
secondary monitor will use the configuration with index 1.
---
Let's say you have more monitors, or you want to make sure that a certain configuration is always applied to a certain
monitor. For this you will want to use the `display_index_preferences`.
Open up a terminal and type the following command: [ `komorebic monitor-info`](../cli/monitor-information.md). This
command will give you the information about your connected monitors, you want to look up the `serial_number_id`. You
should get something like this:
```
komorebic monitor-info
[
{
"id": 6620935,
"name": "DISPLAY1",
"device": "BOE0A1C",
"device_id": "BOE0A1C-5&a2bea0b&0&UID512",
"serial_number_id": "0",
"size": {
"left": 0,
"top": 0,
"right": 1920,
"bottom": 1080
}
},
{
"id": 181932057,
"name": "DISPLAY2",
"device": "VSC8C31",
"device_id": "VSC8C31-5&18560b1f&0&UID4356",
"serial_number_id": "UEP174021562",
"size": {
"left": 0,
"top": -1080,
"right": 1920,
"bottom": 1080
}
}
]
```
In this case the setup is a laptop with a secondary monitor connected. You'll need to figure out which monitor is which,
usually the display name's number should be similar to the numbers you can find on
`Windows Settings -> System -> Display`.
If you have trouble with this step you can always jump on Discord and ask for help (create a `Support` thread).
Once you know which monitor is which, you want to look up their `serial_number_id` to use that on
`display_index_preferences`, you can also use the `device_id`, it accepts both however there have been reported cases
where the `device_id` changes after a restart while the `serial_number_id` doesn't.
So with the example above, we want the laptop to always use the configuration index 0 and the other monitor to use
configuration index 1, so we map the configuration index number to the monitor `serial_number_id`/`device_id` like this:
```json
{
"display_index_preferences": {
"0": "0",
"1": "UEP174021562"
}
}
```
Again you could also have used the `device_id` like this:
```json
{
"display_index_preferences": {
"0": "BOE0A1C-5&a2bea0b&0&UID512",
"1": "VSC8C31-5&18560b1f&0&UID4356"
}
}
```
You should add this `display_index_preferences` option to your `komorebi.json` file. If you find that something is
not working as expected you can try to use the command `komorebic check`.
> [!IMPORTANT]
>
> **When using multiple monitors it is recommended to always set the `display_index_preferences`. If you don't you might
get some undefined behaviour.**
---
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
using the `bar_configurations` array in your `komorebi.json` configuration file. You can refer to the
[multiple-bar-instances](multiple-bar-instances.md) documentation.
In this case it is important to use `display_index_preferences`, because if you don't, and you have 3 or more monitors,
disconnecting and reconnecting monitors may result in the bars for the monitors getting shifted around.
Consider this setup with 3 monitors (A, B and C):
```json
// HOME_MONITOR_1_BAR.json
{
"monitor_index": 0
//...
}
```
```json
// HOME_MONITOR_2_BAR.json
{
"monitor_index": 1
//...
}
```
```json
// WORK_MONITOR_1_BAR.json
{
"monitor_index": 2
//...
}
```
```json
{
"display_index_preferences": {
"0": "MONITOR_1_ID",
"1": "MONITOR_2_ID",
"2": "MONITOR_3_ID"
},
"bar_configurations": [
// this bar uses "monitor_index": 0,
"path/to/bar_config_1.json",
// this bar uses "monitor_index": 1,
"path/to/bar_config_2.json",
// this bar uses "monitor_index": 2,
"path/to/bar_config_3.json"
]
}
```
Komorebi uses an internal map to keep track of monitor to config indices, this map is called `monitor_usr_idx_map` it is
an internal variable to komorebi that you don't need to do anything with, but you can see it with the [
`komorebic state`](../cli/state.md) command (in case you need to debug something).
At first, komorebi will load all monitors and set the internal index map (`monitor_usr_idx_map`) as:
```json
{
// This is monitor A
"0": 0,
// This is monitor B
"1": 1,
// This is monitor C
"2": 2
}
```
Which kind of seems unnecessary, but imagine that then you disconnect monitor B (or it goes to sleep). Then komorebi
will only have 2 monitors with index 0 and 1, so the above map will be updated to this:
```jsonc
[
"0": 0, // This is monitor A
"2": 1, // This is now monitor C, because monitor B disconnected
]
```
So now the bar intended to be for monitor B, which was looking for index "1" on that map, doesn't see it and knows it
should be disabled. And the bar for monitor C looks at that map and knows that it's index "2" now maps to index 1 so it
uses that index internally to get all the correct values about the monitor.
If you didn't have the `display_index_preferences` set, then when you disconnected monitor B, komorebi wouldn't know
how to map the indices and would use default behaviour which would result in a map like this:
```json
{
// This is monitor A
"0": 0,
// This is monitor C, because monitor B disconnected. However the bars will think it is monitor B because it has index "1"
"1": 1
}
```
# Multiple monitors on different machines
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
However, if you do this it is important to be aware of a few things.
Firstly, using `display_index_preferences` is required in this case.
You will need to get the `serial_number_id` or `device_id` of all the monitors of all your setups. With that information
you would then set your config like this:
```json
{
"display_index_preferences": {
"0": "HOME_MONITOR_1_ID",
"1": "HOME_MONITOR_2_ID",
"2": "WORK_MONITOR_1_ID",
"3": "WORK_MONITOR_2_ID"
},
"monitors": [
// HOME_MONITOR_1
{
"workspaces": [
// ...
]
},
// HOME_MONITOR_2
{
"workspaces": [
// ...
]
},
// WORK_MONITOR_1
{
"workspaces": [
// ...
]
},
// WORK_MONITOR_2
{
"workspaces": [
// ...
]
}
]
}
```
> [!NOTE]
>
> *You can't use the same config on two different monitors, you have to make a duplicated config for each monitor!*
Then on the bar configs you need to set the bar's monitor index like this:
```json
// HOME_MONITOR_1_BAR.json
{
"monitor_index": 0
//...
}
```
```json
// HOME_MONITOR_2_BAR.json
{
"monitor_index": 1
//...
}
```
```json
// WORK_MONITOR_1_BAR.json
{
"monitor_index": 2
//...
}
```
```json
// WORK_MONITOR_2_BAR.json
{
"monitor_index": 3
//...
}
```
Although you will only ever have 2 monitors connected at any one time, and they'll always have index 0 and 1, the
above config will still work on both physical configurations.
This is because komorebi will apply the appropriate config to the loaded monitors and will create a map of the user
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
should be enabled, and where it should be drawn.
# Windows Display Settings
In `Settings > System > Display > Multiple Displays`:
- Disable "Remember windows locations on monitor connection"
- Enable "Minimize windows when a monitor is disconnected"
### Things to keep in mind
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
monitor are considered different monitors by komorebi
* When you disconnect from work, komorebi will keep the work monitor cached
* You can still use a laptop alone without any monitor and if you need a window that was on the other monitor you can
press the taskbar icon or use `alt + tab` to bring it to focus and that window will now be part of the laptop monitor
* If you then reconnect the work monitor, the cached version will be applied with all its windows (except any window(s)
you might have moved to another monitor)
* If however, instead of reconnecting the work monitor, you connect the home monitor, then the work monitor will still
remain cached, and komorebi will load the home monitor from the cache (if it exists)
* Sometimes when you disconnect/reconnect a monitor the event might be missed by komorebi, meaning that Windows will
show you both monitors but komorebi won't know about the existence of one of them
* If you notice this type of weird behaviour, always run the [
`komorebic monitor-info`](../cli/monitor-information.md)
command and validate if one of the monitors is missing
* To fix this you can try disconnecting and reconnecting the monitor again, or restarting komorebi

View File

@@ -8,12 +8,8 @@ configuration file.
```json
{
"default_workspace_padding": 0,
"default_container_padding": 0,
"border_width": 0,
"border_offset": -1
"default_container_padding": -1,
}
```
A restart of `komorebi` is required after changing these settings.
[![Watch the tutorial video](https://img.youtube.com/vi/6QYLao953XE/hqdefault.jpg)](https://www.youtube.com/watch?v=6QYLao953XE)

View File

@@ -1,17 +0,0 @@
# Setting a Given Display to a Specific Index
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
configuration option in the static configuration file.
Display IDs can be found using `komorebic monitor-information`.
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
```json
{
"display_index_preferences": {
"0": "DEL4310-5&1a6c0954&0&UID209155",
"1": "<another-display_id>"
}
}
```

View File

@@ -16,6 +16,19 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart
```
## Corporate Devices Enrolled in MDM
If you are using `komorebi` on a corporate device enrolled in mobile device
management, you will receive a pop-up when you run `komorebic start` reminding
you that the [Komorebi License](https://github.com/LGUG2Z/komorebi-license) does
not permit any kind of commercial use.
You can remove this pop-up by running `komorebic license <email>` with the email
associated with your Individual Commercial Use License. A single HTTP request
will be sent with the given email address to verify license validity.
## Starting komorebi
With the example configurations downloaded, you can now start `komorebi`,
`komorebi-bar` and `whkd`.
@@ -23,6 +36,9 @@ With the example configurations downloaded, you can now start `komorebi`,
komorebic start --whkd --bar
```
If you don't want to use the komorebi status bar, you can remove the `--bar` option
from the above command.
## komorebi.json
The example window manager configuration sets some sane defaults and provides
@@ -186,6 +202,9 @@ limitations on hotkey bindings that include the `win` key. However, you will sti
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.
You can toggle an overlay of the current `whkdrc` shortcuts related to `komorebi` at any time when using the example
configuration with `alt + i`.
```
{% include "./whkdrc.sample" %}
```

View File

@@ -34,7 +34,7 @@ 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
`komorebi` is licensed under the [Komorebi 2.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.
@@ -42,7 +42,7 @@ 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 (
The [Komorebi 2.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

View File

@@ -120,6 +120,7 @@ cargo +stable install --path komorebic --locked
cargo +stable install --path komorebic-no-console --locked
cargo +stable install --path komorebi-gui --locked
cargo +stable install --path komorebi-bar --locked
cargo +stable install --path komorebi-shortcuts --locked
```
If the binaries have been built and added to your `$PATH` correctly, you should

View File

@@ -1,6 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.bar.json",
"monitor": 0,
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json",
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",
@@ -48,8 +47,8 @@
{
"Network": {
"enable": true,
"show_total_data_transmitted": true,
"show_network_activity": true
"show_activity": true,
"show_total_activity": true
}
},
{

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
@@ -14,13 +14,6 @@
"unfocused_border": "Base03",
"bar_accent": "Base0D"
},
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300
}
},
"monitors": [
{
"workspaces": [

View File

@@ -132,3 +132,20 @@ running `komorebic stop` and `komorebic start`.
We can see the _komorebi_ state is no longer associated with the previous
device: `null`, suggesting an issue when the display resumes from a suspended
state.
## Komorebi Bar does not render transparency on Nvidia GPUs
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
To solve this the user can do the following:
- Open the Nvidia Control Panel
- On the left menu tree, under "3D Settings", select "Manage 3D Settings"
- Select the "Program Settings" tab
- Press the "Add" button and select "komorebi-bar"
- Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
- Change the setting to "Prefer compatibility"
- At the bottom of the window select "Apply"
- Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
This should resolve the issue and your Komorebi Bar should render with the proper transparency.

View File

@@ -0,0 +1,46 @@
# Focusing Windows
Windows can be focused in a direction (left, down, up, right) using the [`komorebic focus`](../cli/focus.md) command.
```
# example showing how you might bind this command
alt + h : komorebic focus left
alt + j : komorebic focus down
alt + k : komorebic focus up
alt + l : komorebic focus right
```
Windows can be focused in a cycle direction (previous, next) using the [`komorebic cycle-focus`](../cli/cycle-focus.md)
command.
```
# example showing you might bind this command
alt + shift + oem_4 : komorebic cycle-focus previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-focus next # oem_6 is ]
```
It is possible to attempt to focus the first window, on any workspace, matching an exe using the [
`komorebic eager-focus`](../cli/eager-focus.md) command.
```
# example showing how you might bind this command
win + 1 : komorebic eager-focus firefox.exe
```
The window at the largest tile can be focused using the [`komorebic promote-focus`](../cli/promote-focus.md) command.
```
# example showing how you might bind this command
alt + return : komorebic promote-focus
```
The behaviour when attempting to call `komorebic focus` when at the left or right edge of a monitor is determined by
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
When set to `Workspace`, the next workspace on the same monitor will be focused.
When set to `Monitor`, the focused workspace on the next monitor in the given direction will be focused.

View File

@@ -0,0 +1,59 @@
# Focusing Workspaces
Workspaces on the focused monitor can be focused by their index using the [
`komorebic focus-workspace`](../cli/focus-workspace.md) command.
If this command is called with an index for a workspace which does not exist, that workspace, and all workspace indexes
required to get to that workspace, will be created.
```
# example showing how you might bind this command
alt + 1 : komorebic focus-workspace 0
alt + 2 : komorebic focus-workspace 1
alt + 3 : komorebic focus-workspace 2
```
Workspaces on the focused monitor can be focused in a cycle direction (previous, next) using the [
`komorebic cycle-workspace`](../cli/cycle-workspace.md) command.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-workspace previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-workspace next # oem_6 is ]
```
Workspaces on other monitors can be focused by both the monitor index and the workspace index using the [
`komorebic focus-monitor-workspace`](../cli/focus-monitor-workspace.md) command.
```
# example showing how you might bind this command
alt + 1 : komorebic focus-monitor-workspace 0 0
alt + 2 : komorebic focus-monitor-workspace 0 1
alt + 3 : komorebic focus-monitor-workspace 1 0
```
Workspaces on any monitor can be focused by their name (given that all workspace names across all monitors are unique)
using the [`komorebic focus-named-workspace`](../cli/focus-named-workspace.md) command.
```
# example showing how you might bind this command
alt + c : komorebic focus-named-workspace coding
```
Workspaces on all monitors can be set to the same index (emulating single workspaces which span across all monitors)
using the [`komorebic focus-workspaces`](../cli/focus-workspaces.md) command.
```
# example showing how you might bind this command
alt + 1 : komorebic focus-workspaces 0
alt + 2 : komorebic focus-workspaces 1
alt + 3 : komorebic focus-workspaces 2
```
The last focused workspace on the focused monitor can be re-focused using the [
`komorebic focus-last-workspace`](../cli/focus-last-workspace.md) command.

View File

@@ -0,0 +1,59 @@
# Moving Windows Across Workspaces
Windows can be moved to another workspace on the focused monitor using the [
`komorebic move-to-workspace`](../cli/move-to-workspace.md) command. This command will also move your focus to the
target workspace.
```
# example showing how you might bind this command
alt + shift + 1 : komorebic move-to-workspace 0
alt + shift + 2 : komorebic move-to-workspace 1
alt + shift + 3 : komorebic move-to-workspace 2
```
Windows can be sent to another workspace on the focused monitor using the [
`komorebic send-to-workspace`](../cli/send-to-workspace.md) command. This command will keep your focus on the origin
workspace.
```
# example showing how you might bind this command
alt + shift + 1 : komorebic send-to-workspace 0
alt + shift + 2 : komorebic send-to-workspace 1
alt + shift + 3 : komorebic send-to-workspace 2
```
Windows can be moved to another workspace on the focused monitor in a cycle direction (previous, next) using the [
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will also move your focus
to the target workspace.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-move-to-workspace previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-move-to-workspace next # oem_6 is ]
```
Windows can be sent to another workspace on the focused monitor in a cycle direction (previous, next) using the [
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will keep your focus on
the origin workspace.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-send-to-workspace previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-send-to-workspace next # oem_6 is ]
```
Windows can be moved or sent to the focused workspace on a another monitor using the [
`komorebic move-to-monitor`](../cli/move-to-monitor.md) and [`komorebic send-to-monitor`](../cli/send-to-monitor.md)
commands.
Windows can be moved or sent to the focused workspace on a monitor in a cycle direction (previous, next) using the [
`komorebic cycle-move-to-monitor`](../cli/cycle-move-to-monitor.md) and [
`komorebic cycle-send-to-monitor`](../cli/cycle-send-to-monitor.md) commands.
Windows can be moved or sent to a named workspace on any monitor (given that all workspace names across all monitors are
unique) using the [`komorebic move-to-named-workspace`](../cli/move-to-named-workspace.md) and [
`komorebic send-to-named-workspace`](../cli/send-to-named-workspace.md) commands

View File

@@ -0,0 +1,50 @@
# Moving Windows
Windows can be moved in a direction (left, down, up, right) using the [`komorebic move`](../cli/move.md) command.
```
# example showing how you might bind this command
alt + shift + h : komorebic move left
alt + shift + j : komorebic move down
alt + shift + k : komorebic move up
alt + shift + l : komorebic move right
```
Windows can be moved in a cycle direction (previous, next) using the [`komorebic cycle-move`](../cli/cycle-move.md)
command.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-move previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-move next # oem_6 is ]
```
The focused window can be moved to the largest tile using the [`komorebic promote`](../cli/promote.md) command.
```
# example showing how you might bind this command
alt + shift + return : komorebic promote
```
The behaviour when attempting to call `komorebic move` when at the left or right edge of a monitor is determined by
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
When set to `Workspace`, the focused window will be moved to the next workspace on the focused monitor in the given
direction
When set to `Monitor`, the focused window will be moved to the focused workspace on the next monitor in the given
direction.
The behaviour when calling `komorebic move` with `cross_boundary_behaviour` set to `Monitor` can be further refined with
the [`cross_monitor_move_behaviour`](https://komorebi.lgug2z.com/schema#cross_monitor_move_behaviour) configuration
option.
When set to `Swap`, the focused window will be swapped with the window at the corresponding edge of the adjacent monitor
When set to `Insert`, the focused window will be inserted into the focused workspace on the adjacent monitor.
When set to `NoOp`, the focused window will not be moved across a monitor boundary, though focusing across monitor
boundaries will continue to function.

View File

@@ -0,0 +1,52 @@
# Stacking Windows
Windows can be stacked in a direction (left, down, up, right) using the [`komorebic stack`](../cli/stack.md) command.
```
# example showing how you might bind this command
alt + left : komorebic stack left
alt + down : komorebic stack down
alt + up : komorebic stack up
alt + right : komorebic stack right
```
Windows can be popped from a stack using the [`komorebic unstack`](../cli/unstack.md) command.
```
# example showing how you might bind this command
alt + oem_1 : komorebic unstack # oem_1 is ;
```
Windows in a stack can be focused in a cycle direction (previous, next) using the [
`komorebic cycle-stack`](../cli/cycle-stack.md) command.
```
# example showing how you might bind this command
alt + oem_4 : komorebic cycle-stack previous # oem_4 is [
alt + oem_6 : komorebic cycle-stack next # oem_6 is ]
```
Windows in a stack can have their positions in the stack moved in a cycle direction (previous, next) using the [
`komorebic cycle-stack-index`](../cli/cycle-stack-index.md) command.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-stack-index previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-stack-index next # oem_6 is ]
```
Windows in a stack can be focused by their index in the stack using the [
`komorebic focus-stack-window`](../cli/focus-stack-window.md) command.
All windows on the focused workspace can be combined into a single stack using the [
`komorebic stack-all`](../cli/stack-all.md) command.
All windows in a focused stack can be popped using the [`komorebic unstack-all`](../cli/unstack-all.md) command.
It is possible to tell the window manager to stack the next opened window on top of the currently focused window by
using the [
`komorebic toggle-workspace-window-container-behaviour`](../cli/toggle-workspace-window-container-behaviour.md) command.

View File

@@ -5,6 +5,8 @@
alt + o : taskkill /f /im whkd.exe; Start-Process whkd -WindowStyle hidden # if shell is pwsh / powershell
alt + shift + o : komorebic reload-configuration
alt + i : komorebic toggle-shortcuts
# App shortcuts - these require shell to be pwsh / powershell
# The apps will be focused if open, or launched if not open
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }

179
flake.lock generated Normal file
View File

@@ -0,0 +1,179 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1766774972,
"narHash": "sha256-8qxEFpj4dVmIuPn9j9z6NTbU+hrcGjBOvaxTzre5HmM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "01bc1d404a51a0a07e9d8759cd50a7903e218c82",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1765835352,
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks-nix": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1765911976,
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766870016,
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1765674936,
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-parts": "flake-parts",
"git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"treefmt-nix": "treefmt-nix"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1766890375,
"narHash": "sha256-0Zi7ChAtjq/efwQYmp7kOJPcSt6ya9ynSUe6ppgZhsQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "91e1f7a0017065360f447622d11b7ce6ed04772f",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1766000401,
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

360
flake.nix Normal file
View File

@@ -0,0 +1,360 @@
{
description = "komorebi for Windows";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
crane.url = "github:ipetkov/crane";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
git-hooks-nix.url = "github:cachix/git-hooks.nix";
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
inputs@{
self,
nixpkgs,
flake-parts,
crane,
rust-overlay,
...
}:
let
windowsSdkVersion = "10.0.26100";
windowsCrtVersion = "14.44.17.14";
mkWindowsSdk =
pkgs:
pkgs.stdenvNoCC.mkDerivation {
name = "windows-sdk-${windowsSdkVersion}-crt-${windowsCrtVersion}";
nativeBuildInputs = [ pkgs.xwin ];
outputHashAlgo = "sha256";
outputHashMode = "recursive";
outputHash = "sha256-6cLS5q1BDRpLPScfmmKpTTEHUzsgKTKD1+mKvGX9Deo=";
buildCommand = ''
export HOME=$(mktemp -d)
xwin --accept-license \
--sdk-version ${windowsSdkVersion} \
--crt-version ${windowsCrtVersion} \
splat --output $out
'';
};
mkMsvcEnv =
{ pkgs, windowsSdk }:
let
clangVersion = pkgs.lib.versions.major pkgs.llvmPackages.clang.version;
in
{
# linker for the windows target
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER = "lld-link";
# c/c++ compiler
CC_x86_64_pc_windows_msvc = "clang-cl";
CXX_x86_64_pc_windows_msvc = "clang-cl";
AR_x86_64_pc_windows_msvc = "llvm-lib";
# IMPORTANT: libclang include path MUST come first to avoid header conflicts
CFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
"--target=x86_64-pc-windows-msvc"
"-Wno-unused-command-line-argument"
"-fuse-ld=lld-link"
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
"/imsvc${windowsSdk}/crt/include"
"/imsvc${windowsSdk}/sdk/include/ucrt"
"/imsvc${windowsSdk}/sdk/include/um"
"/imsvc${windowsSdk}/sdk/include/shared"
];
CXXFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
"--target=x86_64-pc-windows-msvc"
"-Wno-unused-command-line-argument"
"-fuse-ld=lld-link"
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
"/imsvc${windowsSdk}/crt/include"
"/imsvc${windowsSdk}/sdk/include/ucrt"
"/imsvc${windowsSdk}/sdk/include/um"
"/imsvc${windowsSdk}/sdk/include/shared"
];
# target-specific rust flags with linker flavor and library search paths
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = builtins.concatStringsSep " " [
"-Clinker-flavor=lld-link"
"-Lnative=${windowsSdk}/crt/lib/x86_64"
"-Lnative=${windowsSdk}/sdk/lib/um/x86_64"
"-Lnative=${windowsSdk}/sdk/lib/ucrt/x86_64"
];
# cargo target
CARGO_BUILD_TARGET = "x86_64-pc-windows-msvc";
};
mkKomorebiPackages =
{ pkgs, windowsSdk }:
let
# toolchain with windows msvc target
toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
targets = [ "x86_64-pc-windows-msvc" ];
};
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
version = "0.1.0";
msvcEnv = mkMsvcEnv { inherit pkgs windowsSdk; };
src = pkgs.lib.cleanSourceWith {
src = ./.;
filter =
path: type:
(craneLib.filterCargoSources path type)
|| (pkgs.lib.hasInfix "/docs/" path)
|| (builtins.match ".*/docs/.*" path != null);
};
commonArgs = {
inherit src version;
strictDeps = true;
COMMIT_HASH = self.rev or (pkgs.lib.removeSuffix "-dirty" self.dirtyRev);
# build inputs for cross-compilation
nativeBuildInputs = [
pkgs.llvmPackages.clang-unwrapped
pkgs.llvmPackages.lld
pkgs.llvmPackages.llvm
];
# cross-compilation environment
inherit (msvcEnv)
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
CC_x86_64_pc_windows_msvc
CXX_x86_64_pc_windows_msvc
AR_x86_64_pc_windows_msvc
CFLAGS_x86_64_pc_windows_msvc
CXXFLAGS_x86_64_pc_windows_msvc
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
CARGO_BUILD_TARGET
;
};
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
individualCrateArgs = commonArgs // {
inherit cargoArtifacts;
doCheck = false;
doDoc = false;
};
fullBuild = craneLib.buildPackage (
individualCrateArgs
// {
pname = "komorebi-workspace";
}
);
extractBinary =
binaryName:
pkgs.runCommand "komorebi-${binaryName}"
{
meta = fullBuild.meta // { };
}
''
mkdir -p $out/bin
cp ${fullBuild}/bin/${binaryName}.exe $out/bin/
'';
in
{
inherit
craneLib
src
individualCrateArgs
fullBuild
msvcEnv
;
komorebi = extractBinary "komorebi";
komorebic = extractBinary "komorebic";
komorebic-no-console = extractBinary "komorebic-no-console";
komorebi-bar = extractBinary "komorebi-bar";
komorebi-gui = extractBinary "komorebi-gui";
komorebi-shortcuts = extractBinary "komorebi-shortcuts";
};
mkPkgs =
system:
import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
in
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"aarch64-darwin"
"x86_64-linux"
"aarch64-linux"
];
imports = [
inputs.treefmt-nix.flakeModule
inputs.git-hooks-nix.flakeModule
];
perSystem =
{ config, system, ... }:
let
pkgs = mkPkgs system;
windowsSdk = mkWindowsSdk pkgs;
build = mkKomorebiPackages { inherit pkgs windowsSdk; };
# toolchain with windows target and nightly rustfmt
rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
targets = [ "x86_64-pc-windows-msvc" ];
};
nightlyRustfmt = pkgs.rust-bin.nightly.latest.rustfmt;
rustToolchainWithNightlyRustfmt = pkgs.symlinkJoin {
name = "rust-toolchain-with-nightly-rustfmt";
paths = [
nightlyRustfmt
rustToolchain
];
};
nightlyToolchain = pkgs.rust-bin.nightly.latest.default.override {
targets = [ "x86_64-pc-windows-msvc" ];
};
cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" ''
export PATH="${nightlyToolchain}/bin:$PATH"
exec ${pkgs.cargo-udeps}/bin/cargo-udeps "$@"
'';
in
{
treefmt = {
projectRootFile = "flake.nix";
programs = {
deadnix.enable = true;
just.enable = true;
nixfmt.enable = true;
taplo.enable = true;
rustfmt = {
enable = true;
package = pkgs.rust-bin.nightly.latest.rustfmt;
};
};
};
checks = {
komorebi-workspace-clippy = build.craneLib.cargoClippy (
build.individualCrateArgs
// {
cargoClippyExtraArgs = "--all-targets -- -D warnings";
}
);
komorebi-workspace-fmt = build.craneLib.cargoFmt {
inherit (build) src;
};
komorebi-workspace-toml-fmt = build.craneLib.taploFmt {
src = pkgs.lib.sources.sourceFilesBySuffices build.src [ ".toml" ];
};
komorebi-workspace-deny = build.craneLib.cargoDeny {
inherit (build) src;
};
komorebi-workspace-nextest = build.craneLib.cargoNextest build.individualCrateArgs;
};
packages = {
inherit (build)
komorebi
komorebic
komorebic-no-console
komorebi-bar
komorebi-gui
komorebi-shortcuts
;
inherit windowsSdk;
komorebi-full = build.fullBuild;
default = build.fullBuild;
};
apps = {
komorebi = {
type = "app";
program = "${build.komorebi}/bin/komorebi.exe";
};
komorebic = {
type = "app";
program = "${build.komorebic}/bin/komorebic.exe";
};
komorebic-no-console = {
type = "app";
program = "${build.komorebic-no-console}/bin/komorebic-no-console.exe";
};
komorebi-bar = {
type = "app";
program = "${build.komorebi-bar}/bin/komorebi-bar.exe";
};
komorebi-gui = {
type = "app";
program = "${build.komorebi-gui}/bin/komorebi-gui.exe";
};
komorebi-shortcuts = {
type = "app";
program = "${build.komorebi-shortcuts}/bin/komorebi-shortcuts.exe";
};
default = {
type = "app";
program = "${build.fullBuild}/bin/komorebi.exe";
};
};
devShells.default = pkgs.mkShell {
name = "komorebi";
RUST_BACKTRACE = "full";
# cross-compilation environment
inherit (build.msvcEnv)
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
CC_x86_64_pc_windows_msvc
CXX_x86_64_pc_windows_msvc
AR_x86_64_pc_windows_msvc
CFLAGS_x86_64_pc_windows_msvc
CXXFLAGS_x86_64_pc_windows_msvc
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
CARGO_BUILD_TARGET
;
packages = [
rustToolchainWithNightlyRustfmt
cargo-udeps
# cross-compilation tooling
pkgs.llvmPackages.clang-unwrapped # provides clang-cl
pkgs.llvmPackages.lld # provides lld-link
pkgs.llvmPackages.llvm # provides llvm-lib
pkgs.cargo-deny
pkgs.cargo-nextest
pkgs.cargo-outdated
pkgs.jq
pkgs.just
pkgs.prettier
];
};
pre-commit = {
check.enable = true;
settings.hooks.treefmt = {
enable = true;
package = config.treefmt.build.wrapper;
pass_filenames = false;
};
};
};
};
}

View File

@@ -15,26 +15,38 @@ fmt:
prettier --write .github/FUNDING.yml
prettier --write .github/workflows/windows.yaml
fix:
cargo clippy --fix --allow-dirty
install-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
install-target target:
cargo +stable install --path {{ target }} --locked --no-default-features
install-targets-with-jsonschema *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target-with-jsonschema $_ }
install-target-with-jsonschema target:
cargo +stable install --path {{ target }} --locked
install:
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
install-with-jsonschema:
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
build-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
build-target target:
cargo +stable build --package {{ target }} --locked --profile release-jeezy
cargo +stable build --package {{ target }} --locked --release --no-default-features
build:
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
copy-target target:
cp .\target\release-jeezy\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
cp .\target\release\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
copy-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just copy-target $_ }
@@ -43,10 +55,10 @@ wpm target:
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
copy:
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
run target:
cargo +stable run --bin {{ target }} --locked
cargo +stable run --bin {{ target }} --locked --no-default-features
warn target $RUST_LOG="warn":
just run {{ target }}
@@ -61,21 +73,36 @@ trace target $RUST_LOG="trace":
just run {{ target }}
deadlock $RUST_LOG="trace":
cargo +stable run --bin komorebi --locked --features deadlock_detection
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
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 }
docgen starlight:
rm {{ starlight }}/src/data/cli/windows/*.md
cargo run --package komorebic -- docgen --output {{ starlight }}/src/data/cli/windows
schemars-docgen ./schema.json --output {{ starlight }}/src/content/docs/reference/komorebi-windows.mdx --title "komorebi.json (Windows)" --description "komorebi for Windows configuration schema reference"
schemars-docgen ./schema.bar.json --output {{ starlight }}/src/content/docs/reference/bar-windows.mdx --title "komorebi.bar.json (Windows)" --description "komorebi-bar for Windows configuration schema reference"
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
# 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
version := `cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "komorebi") | .version'`
schemapub:
rm -Force komorebi-schema
mkdir -Force komorebi-schema
cp schema.json komorebi-schema/komorebi.{{ version }}.schema.json
cp schema.bar.json komorebi-schema/komorebi.bar.{{ version }}.schema.json
npx wrangler pages deploy --project-name komorebi --branch main .\komorebi-schema
depcheck:
cargo outdated --depth 2
cargo +nightly udeps --quiet
deps:
cargo update
just depgen
depgen:
cargo deny check
cargo deny list --format json | jq 'del(.unlicensed)' > dependencies.json

View File

@@ -1,14 +1,15 @@
[package]
name = "komorebi-bar"
version = "0.1.35"
edition = "2021"
version = "0.1.40"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-client = { path = "../komorebi-client" }
komorebi-themes = { path = "../komorebi-themes" }
komorebi-client = { path = "../komorebi-client", default-features = false }
komorebi-themes = { path = "../komorebi-themes", default-features = false }
chrono-tz = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true }
color-eyre = { workspace = true }
@@ -16,23 +17,35 @@ crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = "0.9"
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.32"
lazy_static = { workspace = true }
netdev = "0.40"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
parking_lot = { workspace = true }
random_word = { version = "0.5", features = ["en"] }
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
which = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
windows-icons-fallback = { package = "windows-icons", git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
[features]
default = ["schemars"]
schemars = [
"dep:schemars",
"komorebi-client/default",
"komorebi-themes/default",
]

View File

@@ -1,25 +1,28 @@
use crate::config::get_individual_spacing;
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::BAR_HEIGHT;
use crate::DEFAULT_PADDING;
use crate::KomorebiEvent;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::config::MonitorConfigOrIndex;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::config::get_individual_spacing;
use crate::process_hwnd;
use crate::render::Color32Ext;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::KomorebiEvent;
use crate::BAR_HEIGHT;
use crate::DEFAULT_PADDING;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::MonitorInfo;
use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig;
use color_eyre::eyre;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::Align;
@@ -36,6 +39,7 @@ use eframe::egui::Frame;
use eframe::egui::Id;
use eframe::egui::Layout;
use eframe::egui::Margin;
use eframe::egui::PointerButton;
use eframe::egui::Rgba;
use eframe::egui::Style;
use eframe::egui::TextStyle;
@@ -43,20 +47,102 @@ use eframe::egui::Vec2;
use eframe::egui::Visuals;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
use komorebi_client::Colour;
use komorebi_client::MonitorNotification;
use komorebi_client::NotificationEvent;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_client::VirtualDesktopNotification;
use komorebi_themes::Base16Wrapper;
use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use komorebi_themes::KomobarThemeBase16;
use komorebi_themes::KomobarThemeCatppuccin;
use komorebi_themes::KomobarThemeCustom;
use komorebi_themes::catppuccin_egui;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Write;
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::process::ChildStdin;
use std::process::Command;
use std::process::Stdio;
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::atomic::Ordering;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
lazy_static! {
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
}
fn start_powershell() -> eyre::Result<()> {
// found running session, do nothing
if SESSION_STDIN.lock().as_mut().is_some() {
tracing::debug!("PowerShell session already started");
return Ok(());
}
tracing::debug!("Starting PowerShell session");
let mut child = Command::new("powershell.exe")
.args(["-NoLogo", "-NoProfile", "-Command", "-"])
.stdin(Stdio::piped())
.creation_flags(CREATE_NO_WINDOW)
.spawn()?;
let stdin = child.stdin.take().expect("stdin piped");
// Store stdin for later commands
let mut session_stdin = SESSION_STDIN.lock();
*session_stdin = Option::from(stdin);
Ok(())
}
fn stop_powershell() -> eyre::Result<()> {
tracing::debug!("Stopping PowerShell session");
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
if let Err(e) = session_stdin.write_all(b"exit\n") {
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
return Err(e.into());
}
if let Err(e) = session_stdin.flush() {
tracing::error!(error = %e, "failed to flush PowerShell stdin");
return Err(e.into());
}
tracing::debug!("PowerShell session stopped");
} else {
tracing::debug!("PowerShell session already stopped");
}
Ok(())
}
pub fn exec_powershell(cmd: &str) -> eyre::Result<()> {
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
if let Err(e) = writeln!(session_stdin, "{cmd}") {
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
return Err(e.into());
}
if let Err(e) = session_stdin.flush() {
tracing::error!(error = %e, "failed to flush PowerShell stdin");
return Err(e.into());
}
return Ok(());
}
Err(Error::new(ErrorKind::NotFound, "PowerShell session not started").into())
}
pub struct Komobar {
pub hwnd: Option<isize>,
@@ -64,7 +150,7 @@ pub struct Komobar {
pub disabled: bool,
pub config: KomobarConfig,
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub center_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
@@ -76,6 +162,18 @@ pub struct Komobar {
pub size_rect: komorebi_client::Rect,
pub work_area_offset: komorebi_client::Rect,
applied_theme_on_first_frame: bool,
mouse_follows_focus: bool,
input_config: InputConfig,
}
struct InputConfig {
accumulated_scroll_delta: Vec2,
act_on_vertical_scroll: bool,
act_on_horizontal_scroll: bool,
vertical_scroll_threshold: f32,
horizontal_scroll_threshold: f32,
vertical_scroll_max_threshold: f32,
horizontal_scroll_max_threshold: f32,
}
pub fn apply_theme(
@@ -87,75 +185,86 @@ pub fn apply_theme(
grouping: Option<Grouping>,
render_config: Rc<RefCell<RenderConfig>>,
) {
match theme {
KomobarTheme::Catppuccin {
let (auto_select_fill, auto_select_text) = match theme {
KomobarTheme::Catppuccin(KomobarThemeCatppuccin {
name: catppuccin,
accent: catppuccin_value,
} => match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
auto_select_fill: catppuccin_auto_select_fill,
auto_select_text: catppuccin_auto_select_text,
}) => {
match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::FRAPPE.base);
bg_color.replace(catppuccin_egui::FRAPPE.base);
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
},
KomobarTheme::Base16 {
(
catppuccin_auto_select_fill.map(|c| c.color32(catppuccin.as_theme())),
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
)
}
KomobarTheme::Base16(KomobarThemeBase16 {
name: base16,
accent: base16_value,
} => {
auto_select_fill: base16_auto_select_fill,
auto_select_text: base16_auto_select_text,
}) => {
ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(base16);
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
@@ -164,8 +273,46 @@ pub fn apply_theme(
});
bg_color.replace(base16.background());
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Base16(base16))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
)
}
}
KomobarTheme::Custom(KomobarThemeCustom {
colours,
accent: base16_value,
auto_select_fill: base16_auto_select_fill,
auto_select_text: base16_auto_select_text,
}) => {
let background = colours.background();
ctx.set_style(colours.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(Base16Wrapper::Custom(colours.clone()));
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
});
bg_color.replace(background);
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
)
}
};
AUTO_SELECT_FILL_COLOUR.store(
auto_select_fill.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
AUTO_SELECT_TEXT_COLOUR.store(
auto_select_text.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
// Apply transparency_alpha
let theme_color = *bg_color.borrow();
@@ -175,16 +322,15 @@ pub fn apply_theme(
// apply rounding to the widgets
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
&grouping
&& let Some(rounding) = config.rounding
{
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
}
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
}
// Update RenderConfig's background_color so that widgets will have the new color
@@ -195,7 +341,7 @@ impl Komobar {
pub fn apply_config(
&mut self,
ctx: &Context,
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
) {
MAX_LABEL_WIDTH.store(
self.config.max_label_width.unwrap_or(400.0) as i32,
@@ -224,7 +370,7 @@ impl Komobar {
self.config.icon_scale,
));
let mut komorebi_notification_state = previous_notification_state;
let mut monitor_info = previous_monitor_info;
let mut komorebi_widgets = Vec::new();
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
@@ -276,19 +422,18 @@ impl Komobar {
komorebi_widgets
.into_iter()
.for_each(|(mut widget, idx, side)| {
match komorebi_notification_state {
match monitor_info {
None => {
komorebi_notification_state =
Some(widget.komorebi_notification_state.clone());
monitor_info = Some(widget.monitor_info.clone());
}
Some(ref previous) => {
if widget.workspaces.is_some_and(|w| w.enable) {
previous.borrow_mut().update_from_config(
&widget.komorebi_notification_state.borrow(),
);
if widget.workspaces.is_some() {
previous
.borrow_mut()
.update_from_self(&widget.monitor_info.borrow());
}
widget.komorebi_notification_state = previous.clone();
widget.monitor_info = previous.clone();
}
}
@@ -308,20 +453,25 @@ impl Komobar {
self.right_widgets = right_widgets;
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
(monitor_config.index, monitor_config.work_area_offset)
}
MonitorConfigOrIndex::Index(idx) => (*idx, None),
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
None => (0, None),
};
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
state
.borrow()
.monitor_usr_idx_map
.get(&usr_monitor_index)
.copied()
let mapped_info = self.monitor_info.as_ref().map(|info| {
let monitor = info.borrow();
(
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
monitor.mouse_follows_focus,
)
});
self.monitor_index = monitor_index;
if let Some(info) = mapped_info {
self.monitor_index = info.0;
self.mouse_follows_focus = info.1;
}
if let Some(monitor_index) = self.monitor_index {
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
@@ -372,17 +522,51 @@ impl Komobar {
}
}
}
} else if self.komorebi_notification_state.is_some() && !self.disabled {
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
} else if self.monitor_info.is_some() && !self.disabled {
tracing::warn!(
"couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..."
);
self.disabled = true;
} else {
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
tracing::warn!(
"couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi."
);
self.disabled = true;
}
if let Some(mouse) = &self.config.mouse {
self.input_config.act_on_vertical_scroll =
mouse.on_scroll_up.is_some() || mouse.on_scroll_down.is_some();
self.input_config.act_on_horizontal_scroll =
mouse.on_scroll_left.is_some() || mouse.on_scroll_right.is_some();
self.input_config.vertical_scroll_threshold = mouse
.vertical_scroll_threshold
.unwrap_or(30.0)
.clamp(10.0, 300.0);
self.input_config.horizontal_scroll_threshold = mouse
.horizontal_scroll_threshold
.unwrap_or(30.0)
.clamp(10.0, 300.0);
// limit how many "ticks" can be accumulated
self.input_config.vertical_scroll_max_threshold =
self.input_config.vertical_scroll_threshold * 3.0;
self.input_config.horizontal_scroll_max_threshold =
self.input_config.horizontal_scroll_threshold * 3.0;
if mouse.has_command() {
start_powershell().unwrap_or_else(|_| {
tracing::error!("failed to start powershell session");
});
} else {
stop_powershell().unwrap_or_else(|_| {
tracing::error!("failed to stop powershell session");
});
}
}
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
self.monitor_info = monitor_info;
}
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
@@ -419,7 +603,9 @@ impl Komobar {
end.x -= margin.left + margin.right;
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")
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 {
@@ -431,11 +617,11 @@ impl Komobar {
}
fn try_apply_theme(&mut self, ctx: &Context) {
match self.config.theme {
match &self.config.theme {
Some(theme) => {
apply_theme(
ctx,
theme,
theme.clone(),
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
@@ -447,13 +633,15 @@ impl Komobar {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
);
home
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
@@ -472,21 +660,6 @@ impl Komobar {
bar_grouping,
self.render_config.clone(),
);
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
name, stack_border, ..
} => stack_border
.unwrap_or(CatppuccinValue::Green)
.color32(name.as_theme()),
KomorebiTheme::Base16 {
name, stack_border, ..
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
};
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
}
}
Err(_) => {
@@ -499,17 +672,16 @@ impl Komobar {
| Grouping::Alignment(config)
| Grouping::Widget(config),
) = &bar_grouping
&& let Some(rounding) = config.rounding
{
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius =
rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
}
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius =
rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
}
}
}
@@ -529,7 +701,7 @@ impl Komobar {
disabled: false,
config,
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
monitor_info: None,
left_widgets: vec![],
center_widgets: vec![],
right_widgets: vec![],
@@ -541,6 +713,16 @@ impl Komobar {
size_rect: komorebi_client::Rect::default(),
work_area_offset: komorebi_client::Rect::default(),
applied_theme_on_first_frame: false,
mouse_follows_focus: false,
input_config: InputConfig {
accumulated_scroll_delta: Vec2::new(0.0, 0.0),
act_on_vertical_scroll: false,
act_on_horizontal_scroll: false,
vertical_scroll_threshold: 0.0,
horizontal_scroll_threshold: 0.0,
vertical_scroll_max_threshold: 0.0,
horizontal_scroll_max_threshold: 0.0,
},
};
komobar.apply_config(&cc.egui_ctx, None);
@@ -665,12 +847,12 @@ impl eframe::App for Komobar {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(ctx, self.komorebi_notification_state.clone());
self.apply_config(ctx, self.monitor_info.clone());
}
if let Ok(updated_config) = self.rx_config.try_recv() {
self.config = updated_config;
self.apply_config(ctx, self.komorebi_notification_state.clone());
self.apply_config(ctx, self.monitor_info.clone());
}
match self.rx_gui.try_recv() {
@@ -685,13 +867,41 @@ impl eframe::App for Komobar {
Ok(KomorebiEvent::Notification(notification)) => {
let state = &notification.state;
let usr_monitor_index = match &self.config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
MonitorConfigOrIndex::Index(idx) => *idx,
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
monitor_config.index
}
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
};
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
self.monitor_index = monitor_index;
let mut should_apply_config = false;
match notification.event {
NotificationEvent::VirtualDesktop(
VirtualDesktopNotification::EnteredAssociatedVirtualDesktop,
) => {
tracing::debug!(
"back on komorebi's associated virtual desktop - restoring bar"
);
if let Some(hwnd) = self.hwnd {
komorebi_client::WindowsApi::restore_window(hwnd);
}
}
NotificationEvent::VirtualDesktop(
VirtualDesktopNotification::LeftAssociatedVirtualDesktop,
) => {
tracing::debug!(
"no longer on komorebi's associated virtual desktop - minimizing bar"
);
if let Some(hwnd) = self.hwnd {
komorebi_client::WindowsApi::minimize_window(hwnd);
}
}
_ => {}
}
if self.monitor_index.is_none()
|| self
.monitor_index
@@ -736,9 +946,9 @@ impl eframe::App for Komobar {
) {
let monitor_index = self.monitor_index.expect("should have a monitor index");
let monitor_size = state.monitors.elements()[monitor_index].size();
let monitor_size = state.monitors.elements()[monitor_index].size;
self.update_monitor_coordinates(monitor_size);
self.update_monitor_coordinates(&monitor_size);
should_apply_config = true;
}
@@ -749,7 +959,7 @@ impl eframe::App for Komobar {
// Check if monitor coordinates/size has changed
if let Some(monitor_index) = self.monitor_index {
let monitor_size = state.monitors.elements()[monitor_index].size();
let monitor_size = state.monitors.elements()[monitor_index].size;
let top = MONITOR_TOP.load(Ordering::SeqCst);
let left = MONITOR_LEFT.load(Ordering::SeqCst);
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
@@ -759,36 +969,38 @@ impl eframe::App for Komobar {
bottom: monitor_size.bottom,
right,
};
if *monitor_size != rect {
if monitor_size != rect {
tracing::info!(
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
monitor_size
);
self.update_monitor_coordinates(monitor_size);
self.update_monitor_coordinates(&monitor_size);
should_apply_config = true;
}
}
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(
ctx,
self.monitor_index,
notification,
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 let Some(monitor_info) = &self.monitor_info {
monitor_info.borrow_mut().update(
self.monitor_index,
notification.state,
self.render_config.borrow().show_all_icons,
);
handle_notification(
ctx,
notification.event,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme.clone(),
self.render_config.clone(),
);
}
if should_apply_config {
self.apply_config(ctx, self.komorebi_notification_state.clone());
self.apply_config(ctx, self.monitor_info.clone());
// Reposition the Bar
self.position_bar();
@@ -870,6 +1082,111 @@ impl eframe::App for Komobar {
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
CentralPanel::default().frame(frame).show(ctx, |ui| {
if let Some(mouse_config) = &self.config.mouse {
let command = if ui
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
{
tracing::debug!("Input: primary button double clicked");
&mouse_config.on_primary_double_click
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Secondary)) {
tracing::debug!("Input: secondary button clicked");
&mouse_config.on_secondary_click
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Middle)) {
tracing::debug!("Input: middle button clicked");
&mouse_config.on_middle_click
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra1)) {
tracing::debug!("Input: extra1 button clicked");
&mouse_config.on_extra1_click
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra2)) {
tracing::debug!("Input: extra2 button clicked");
&mouse_config.on_extra2_click
} else if self.input_config.act_on_vertical_scroll
|| self.input_config.act_on_horizontal_scroll
{
let scroll_delta = ui.input(|input| input.smooth_scroll_delta);
self.input_config.accumulated_scroll_delta += scroll_delta;
if scroll_delta.y != 0.0 && self.input_config.act_on_vertical_scroll {
// Do not store more than the max threshold
self.input_config.accumulated_scroll_delta.y =
self.input_config.accumulated_scroll_delta.y.clamp(
-self.input_config.vertical_scroll_max_threshold,
self.input_config.vertical_scroll_max_threshold,
);
// When the accumulated scroll passes the threshold, trigger a tick.
if self.input_config.accumulated_scroll_delta.y.abs()
>= self.input_config.vertical_scroll_threshold
{
let direction_command =
if self.input_config.accumulated_scroll_delta.y > 0.0 {
&mouse_config.on_scroll_up
} else {
&mouse_config.on_scroll_down
};
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
self.input_config.accumulated_scroll_delta.y -=
self.input_config.vertical_scroll_threshold
* self.input_config.accumulated_scroll_delta.y.signum();
tracing::debug!(
"Input: vertical scroll ticked. excess: {} | threshold: {}",
self.input_config.accumulated_scroll_delta.y,
self.input_config.vertical_scroll_threshold
);
direction_command
} else {
&None
}
} else if scroll_delta.x != 0.0 && self.input_config.act_on_horizontal_scroll {
// Do not store more than the max threshold
self.input_config.accumulated_scroll_delta.x =
self.input_config.accumulated_scroll_delta.x.clamp(
-self.input_config.horizontal_scroll_max_threshold,
self.input_config.horizontal_scroll_max_threshold,
);
// When the accumulated scroll passes the threshold, trigger a tick.
if self.input_config.accumulated_scroll_delta.x.abs()
>= self.input_config.horizontal_scroll_threshold
{
let direction_command =
if self.input_config.accumulated_scroll_delta.x > 0.0 {
&mouse_config.on_scroll_left
} else {
&mouse_config.on_scroll_right
};
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
self.input_config.accumulated_scroll_delta.x -=
self.input_config.horizontal_scroll_threshold
* self.input_config.accumulated_scroll_delta.x.signum();
tracing::debug!(
"Input: horizontal scroll ticked. excess: {} | threshold: {}",
self.input_config.accumulated_scroll_delta.x,
self.input_config.horizontal_scroll_threshold
);
direction_command
} else {
&None
}
} else {
&None
}
} else {
&None
};
if let Some(command) = command {
command.execute(self.mouse_follows_focus);
}
}
// Apply grouping logic for the bar as a whole
let area_frame = if let Some(frame) = &self.config.frame {
Frame::NONE
@@ -1009,3 +1326,66 @@ pub enum Alignment {
Center,
Right,
}
#[allow(clippy::too_many_arguments)]
fn handle_notification(
ctx: &Context,
event: komorebi_client::NotificationEvent,
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>>,
) {
if let NotificationEvent::Socket(message) = event {
match message {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color.clone(),
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,
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
}
}
}

View File

@@ -1,21 +1,24 @@
use crate::render::Grouping;
use crate::widget::WidgetConfig;
use crate::DEFAULT_PADDING;
use crate::bar::exec_powershell;
use crate::render::Grouping;
use crate::widgets::widget::WidgetConfig;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::KomorebiTheme;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use schemars::JsonSchema;
use komorebi_client::SocketMessage;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.35`
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.bar.json` configuration file reference for `v0.1.40`
pub struct KomobarConfig {
/// Bar height (default: 50)
/// Bar height
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50.0)))]
pub height: Option<f32>,
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
/// vertical definition which can each take a single value for a symmetric padding or two
@@ -73,23 +76,31 @@ pub struct KomobarConfig {
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// The monitor index or the full monitor options
pub monitor: MonitorConfigOrIndex,
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
pub monitor: Option<MonitorConfigOrIndex>,
/// Font family
pub font_family: Option<String>,
/// Font size (default: 12.5)
/// Font size
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
pub font_size: Option<f32>,
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
/// Scale of the icons relative to the font_size [[1.0-2.0]]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1.4)))]
pub icon_scale: Option<f32>,
/// Max label width before text truncation (default: 400.0)
/// Max label width before text truncation
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Alpha value for the color transparency [[0-255]] (default: 200)
/// Alpha value for the color transparency [[0-255]]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
pub transparency_alpha: Option<u8>,
/// Spacing between widgets (default: 10.0)
/// Spacing between widgets
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
pub widget_spacing: Option<f32>,
/// Visual grouping for widgets
pub grouping: Option<Grouping>,
/// Options for mouse interaction on the bar
pub mouse: Option<MouseConfig>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right)
@@ -115,7 +126,9 @@ impl KomobarConfig {
}
if display {
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
println!(
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
);
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
@@ -136,7 +149,9 @@ impl KomobarConfig {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position configuration
pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
#[serde(alias = "position")]
@@ -146,14 +161,18 @@ pub struct PositionConfig {
pub end: Option<Position>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Frame configuration
pub struct FrameConfig {
/// Margin inside the painted frame
pub inner_margin: Position,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Monitor configuration or monitor index
pub enum MonitorConfigOrIndex {
/// The monitor index where you want the bar to show
Index(usize),
@@ -161,7 +180,9 @@ pub enum MonitorConfigOrIndex {
MonitorConfig(MonitorConfig),
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Monitor configuration
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub index: usize,
@@ -172,15 +193,20 @@ pub struct MonitorConfig {
pub type Padding = SpacingKind;
pub type Margin = SpacingKind;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
// WARNING: To any developer messing with this code in the future: The order here matters!
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
// individual values.
/// Spacing kind
pub enum SpacingKind {
/// Spacing applied to all sides
All(f32),
/// Individual spacing applied to each side
Individual(IndividualSpacingConfig),
/// Grouped vertical and horizontal spacing
Grouped(GroupedSpacingConfig),
}
@@ -223,24 +249,38 @@ impl SpacingKind {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouped vertical and horizontal spacing
pub struct GroupedSpacingConfig {
/// Vertical grouped spacing
pub vertical: Option<GroupedSpacingOptions>,
/// Horizontal grouped spacing
pub horizontal: Option<GroupedSpacingOptions>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Grouped spacing options
pub enum GroupedSpacingOptions {
/// Symmetrical grouped spacing
Symmetrical(f32),
/// Split grouped spacing
Split(f32, f32),
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Individual spacing configuration
pub struct IndividualSpacingConfig {
/// Spacing for the top
pub top: f32,
/// Spacing for the bottom
pub bottom: f32,
/// Spacing for the left
pub left: f32,
/// Spacing for the right
pub right: f32,
}
@@ -317,6 +357,153 @@ pub fn get_individual_spacing(
})
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Mouse message
pub enum MouseMessage {
/// Send a message to the komorebi client.
/// By default, a batch of messages are sent in the following order:
/// FocusMonitorAtCursor =>
/// MouseFollowsFocus(false) =>
/// {message} =>
/// MouseFollowsFocus({original.value})
///
/// Example:
/// ```json
/// "on_extra2_click": {
/// "message": {
/// "type": "NewWorkspace"
/// }
/// },
/// ```
/// or:
/// ```json
/// "on_middle_click": {
/// "focus_monitor_at_cursor": false,
/// "ignore_mouse_follows_focus": false,
/// "message": {
/// "type": "TogglePause"
/// }
/// }
/// ```
/// or:
/// ```json
/// "on_scroll_up": {
/// "message": {
/// "type": "CycleFocusWorkspace",
/// "content": "Previous"
/// }
/// }
/// ```
Komorebi(KomorebiMouseMessage),
/// Execute a custom command.
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
/// Example: `komorebic toggle-pause`
Command(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi socket mouse message
pub struct KomorebiMouseMessage {
/// Send the FocusMonitorAtCursor message
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub focus_monitor_at_cursor: Option<bool>,
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub ignore_mouse_follows_focus: Option<bool>,
/// The message to send to the komorebi client
pub message: komorebi_client::SocketMessage,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Mouse configuration
pub struct MouseConfig {
/// Command to send on primary/left double button click
pub on_primary_double_click: Option<MouseMessage>,
/// Command to send on secondary/right button click
pub on_secondary_click: Option<MouseMessage>,
/// Command to send on middle button click
pub on_middle_click: Option<MouseMessage>,
/// Command to send on extra1/back button click
pub on_extra1_click: Option<MouseMessage>,
/// Command to send on extra2/forward button click
pub on_extra2_click: Option<MouseMessage>,
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
pub vertical_scroll_threshold: Option<f32>,
/// Command to send on scrolling up (every tick)
pub on_scroll_up: Option<MouseMessage>,
/// Command to send on scrolling down (every tick)
pub on_scroll_down: Option<MouseMessage>,
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
pub horizontal_scroll_threshold: Option<f32>,
/// Command to send on scrolling left (every tick)
pub on_scroll_left: Option<MouseMessage>,
/// Command to send on scrolling right (every tick)
pub on_scroll_right: Option<MouseMessage>,
}
impl MouseConfig {
pub fn has_command(&self) -> bool {
[
&self.on_primary_double_click,
&self.on_secondary_click,
&self.on_middle_click,
&self.on_extra1_click,
&self.on_extra2_click,
&self.on_scroll_up,
&self.on_scroll_down,
&self.on_scroll_left,
&self.on_scroll_right,
]
.iter()
.any(|opt| matches!(opt, Some(MouseMessage::Command(_))))
}
}
impl MouseMessage {
pub fn execute(&self, mouse_follows_focus: bool) {
match self {
MouseMessage::Komorebi(config) => {
let mut messages = Vec::new();
if config.focus_monitor_at_cursor.unwrap_or(true) {
messages.push(SocketMessage::FocusMonitorAtCursor);
}
if config.ignore_mouse_follows_focus.unwrap_or(true) {
messages.push(SocketMessage::MouseFollowsFocus(false));
messages.push(config.message.clone());
messages.push(SocketMessage::MouseFollowsFocus(mouse_follows_focus));
} else {
messages.push(config.message.clone());
}
tracing::debug!("Sending messages: {messages:?}");
if komorebi_client::send_batch(messages).is_err() {
tracing::error!("could not send commands");
}
}
MouseMessage::Command(cmd) => {
tracing::debug!("Executing command: {}", cmd);
let cmd_no_env = cmd.replace_env();
if exec_powershell(cmd_no_env.to_str().expect("Invalid command")).is_err() {
tracing::error!("Failed to execute '{}'", cmd);
}
}
};
}
}
impl KomobarConfig {
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
let content = std::fs::read_to_string(path)?;
@@ -338,7 +525,9 @@ impl KomobarConfig {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position
pub struct Position {
/// X coordinate
pub x: f32,
@@ -364,43 +553,11 @@ impl From<Position> for Pos2 {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "palette")]
pub enum KomobarTheme {
/// A theme from catppuccin-egui
Catppuccin {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin,
accent: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
},
}
pub use komorebi_themes::KomobarTheme;
impl From<KomorebiTheme> for KomobarTheme {
fn from(value: KomorebiTheme) -> Self {
match value {
KomorebiTheme::Catppuccin {
name, bar_accent, ..
} => Self::Catppuccin {
name,
accent: bar_accent,
},
KomorebiTheme::Base16 {
name, bar_accent, ..
} => Self::Base16 {
name,
accent: bar_accent,
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Label prefix
pub enum LabelPrefix {
/// Show no prefix
None,
@@ -412,7 +569,9 @@ pub enum LabelPrefix {
IconAndText,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Display format
pub enum DisplayFormat {
/// Show only icon
Icon,
@@ -427,8 +586,10 @@ pub enum DisplayFormat {
}
macro_rules! extend_enum {
($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq)]
($(#[$type_meta:meta])* $existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
$(#[$type_meta])*
pub enum $new_enum {
// Add new variants
$(
@@ -449,7 +610,9 @@ macro_rules! extend_enum {
};
}
extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
extend_enum!(
/// Workspaces display format
DisplayFormat, WorkspacesDisplayFormat, {
/// Show all icons only
AllIcons,
/// Show both all icons and text
@@ -460,12 +623,12 @@ extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
#[cfg(test)]
mod tests {
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum OriginalDisplayFormat {
/// Show None Of The Things
NoneOfTheThings,

View File

@@ -1,821 +0,0 @@
use crate::bar::apply_theme;
use crate::config::DisplayFormat;
use crate::config::KomobarTheme;
use crate::config::WorkspacesDisplayFormat;
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 eframe::egui::vec2;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::Margin;
use eframe::egui::RichText;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use image::RgbaImage;
use komorebi_client::Container;
use komorebi_client::NotificationEvent;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_client::Window;
use komorebi_client::Workspace;
use komorebi_client::WorkspaceLayer;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: Option<KomorebiWorkspacesConfig>,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Workspace Layer widget
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
/// Configure the Focused Window widget
pub focused_window: Option<KomorebiFocusedWindowConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
/// Display format of the workspace
pub display: Option<WorkspacesDisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
/// List of layout options
pub options: Option<Vec<KomorebiLayout>>,
/// Display format of the current layout
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiWorkspaceLayerConfig {
/// Enable the Komorebi Workspace Layer widget
pub enable: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
pub enable: bool,
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
pub show_icon: Option<bool>,
/// Display format of the currently focused window
pub display: Option<DisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
/// A map of display friendly name => path to configuration.json
pub configurations: BTreeMap<String, String>,
}
impl From<&KomorebiConfig> for Komorebi {
fn from(value: &KomorebiConfig) -> Self {
let configuration_switcher =
if let Some(configuration_switcher) = &value.configuration_switcher {
let mut configuration_switcher = configuration_switcher.clone();
for (_, location) in configuration_switcher.configurations.iter_mut() {
*location = dunce::simplified(&PathBuf::from(location.clone()).replace_env())
.to_string_lossy()
.to_string();
}
Some(configuration_switcher)
} else {
None
};
Self {
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
selected_workspace: String::new(),
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
workspaces: vec![],
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,
stack_accent: None,
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
monitor_usr_idx_map: HashMap::new(),
})),
workspaces: value.workspaces,
layout: value.layout.clone(),
focused_window: value.focused_window,
workspace_layer: value.workspace_layer,
configuration_switcher,
}
}
}
#[derive(Clone, Debug)]
pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: Option<KomorebiWorkspacesConfig>,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
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);
let text_size = Vec2::splat(config.text_font_id.size);
if let Some(workspaces) = self.workspaces {
if workspaces.enable {
let mut update = None;
if !komorebi_notification_state.workspaces.is_empty() {
let format = workspaces.display.unwrap_or(DisplayFormat::Text.into());
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, containers, _)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
let is_selected = komorebi_notification_state.selected_workspace.eq(ws);
if SelectableFrame::new(
is_selected,
)
.show(ui, |ui| {
let mut has_icon = false;
if format == WorkspacesDisplayFormat::AllIcons
|| format == WorkspacesDisplayFormat::AllIconsAndText
|| format == WorkspacesDisplayFormat::AllIconsAndTextOnSelected
|| format == DisplayFormat::Icon.into()
|| format == DisplayFormat::IconAndText.into()
|| format == DisplayFormat::IconAndTextOnSelected.into()
|| (format == DisplayFormat::TextAndIconOnSelected.into() && is_selected)
{
has_icon = containers.iter().any(|(_, container_info)| {
container_info.icons.iter().any(|icon| icon.is_some())
});
if has_icon {
Frame::NONE
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y as i8,
))
.show(ui, |ui| {
for (is_focused, container) in containers {
for icon in container.icons.iter().flatten().collect::<Vec<_>>() {
ui.add(
Image::from(&img_to_texture(ctx, icon))
.maintain_aspect_ratio(true)
.fit_to_exact_size(if *is_focused { icon_size } else { text_size }),
);
}
}
});
}
}
// draw a custom icon when there is no app icon or text
if !has_icon && (matches!(format, WorkspacesDisplayFormat::AllIcons | WorkspacesDisplayFormat::Existing(DisplayFormat::Icon))
|| (!is_selected && matches!(format, WorkspacesDisplayFormat::AllIconsAndTextOnSelected | WorkspacesDisplayFormat::Existing(DisplayFormat::IconAndTextOnSelected)))) {
let (response, painter) =
ui.allocate_painter(icon_size, Sense::hover());
let stroke = Stroke::new(
1.0,
if is_selected { ctx.style().visuals.selection.stroke.color} else { ui.style().visuals.text_color() },
);
let mut rect = response.rect;
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
response.on_hover_text(ws.to_string())
// add hover text when there are only icons
} else if match format {
WorkspacesDisplayFormat::AllIcons | WorkspacesDisplayFormat::Existing(DisplayFormat::Icon) => has_icon,
_ => false,
} {
ui.response().on_hover_text(ws.to_string())
// add label only
} else if (format != WorkspacesDisplayFormat::AllIconsAndTextOnSelected && format != DisplayFormat::IconAndTextOnSelected.into())
|| (is_selected && matches!(format, WorkspacesDisplayFormat::AllIconsAndTextOnSelected | WorkspacesDisplayFormat::Existing(DisplayFormat::IconAndTextOnSelected)))
{
if is_selected {
ui.add(Label::new(RichText::new(ws.to_string()).color(ctx.style().visuals.selection.stroke.color)).selectable(false))
}
else {
ui.add(Label::new(ws.to_string()).selectable(false))
}
} else {
ui.response()
}
})
.clicked()
{
update = Some(ws.to_string());
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,
),
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(layer_config) = &self.workspace_layer {
if layer_config.enable {
let layer = komorebi_notification_state
.workspaces
.iter()
.find(|o| komorebi_notification_state.selected_workspace.eq(&o.0))
.map(|(_, _, layer)| layer);
if let Some(layer) = layer {
let name = layer.to_string();
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(name).selectable(false)))
.clicked()
&& komorebi_client::send_batch([
SocketMessage::MouseFollowsFocus(false),
SocketMessage::ToggleWorkspaceLayer,
SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
),
])
.is_err()
{
tracing::error!(
"could not send the following batch of messages to komorebi:\n\
MouseFollowsFocus(false),
ToggleWorkspaceLayer,
MouseFollowsFocus({})",
komorebi_notification_state.mouse_follows_focus,
);
}
});
}
}
}
if let Some(layout_config) = &self.layout {
if layout_config.enable {
let workspace_idx: Option<usize> = komorebi_notification_state
.workspaces
.iter()
.position(|o| komorebi_notification_state.selected_workspace.eq(&o.0));
komorebi_notification_state.layout.show(
ctx,
ui,
config,
layout_config,
workspace_idx,
);
}
}
if let Some(configuration_switcher) = &self.configuration_switcher {
if configuration_switcher.enable {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file() {
config.apply_on_widget(false, ui,|ui|{
if SelectableFrame::new(false).show(ui, |ui|{
ui.add(Label::new(name).selectable(false))
})
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
canonicalized,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
proceed = false;
}
if let Some(rect) = komorebi_notification_state.work_area_offset {
if proceed {
match komorebi_client::send_query(&SocketMessage::Query(
komorebi_client::StateQuery::FocusedMonitorIndex,
)) {
Ok(idx) => {
if let Ok(monitor_idx) = idx.parse::<usize>() {
if komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
rect,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MonitorWorkAreaOffset"
);
}
}
}
Err(_) => {
tracing::error!(
"could not send message to komorebi: Query"
);
}
}
}
}
}});
}
}
}
}
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
let titles = &komorebi_notification_state
.focused_container_information
.titles;
if !titles.is_empty() {
config.apply_on_widget(false, ui, |ui| {
let icons = &komorebi_notification_state
.focused_container_information
.icons;
let focused_window_idx = komorebi_notification_state
.focused_container_information
.focused_window_idx;
let iter = titles.iter().zip(icons.iter());
let len = iter.len();
for (i, (title, icon)) in iter.enumerate() {
let selected = i == focused_window_idx && len != 1;
let text_color = if selected { ctx.style().visuals.selection.stroke.color} else { ui.style().visuals.text_color() };
if SelectableFrame::new(selected)
.show(ui, |ui| {
// handle legacy setting
let format = focused_window.display.unwrap_or(
if focused_window.show_icon.unwrap_or(false) {
DisplayFormat::IconAndText
} else {
DisplayFormat::Text
},
);
if format == DisplayFormat::Icon
|| format == DisplayFormat::IconAndText
|| format == DisplayFormat::IconAndTextOnSelected
|| (format == DisplayFormat::TextAndIconOnSelected
&& i == focused_window_idx)
{
if let Some(img) = icon {
Frame::NONE
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y as i8,
))
.show(ui, |ui| {
let response = ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.fit_to_exact_size(icon_size),
);
if let DisplayFormat::Icon = format {
response.on_hover_text(title);
}
});
}
}
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);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(RichText::new( title).color(text_color)).selectable(false).truncate(),
);
}
})
.clicked()
{
if selected {
return;
}
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"
);
}
}
}
});
}
}
}
}
}
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
ctx.load_texture("icon", color_image, TextureOptions::default())
}
#[allow(clippy::type_complexity)]
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<(
String,
Vec<(bool, KomorebiNotificationStateContainerInformation)>,
WorkspaceLayer,
)>,
pub selected_workspace: String,
pub focused_container_information: KomorebiNotificationStateContainerInformation,
pub layout: KomorebiLayout,
pub hide_empty_workspaces: bool,
pub mouse_follows_focus: bool,
pub work_area_offset: Option<Rect>,
pub stack_accent: Option<Color32>,
pub monitor_index: usize,
pub monitor_usr_idx_map: HashMap<usize, usize>,
}
impl KomorebiNotificationState {
pub fn update_from_config(&mut self, config: &Self) {
self.hide_empty_workspaces = config.hide_empty_workspaces;
}
#[allow(clippy::too_many_arguments)]
pub fn handle_notification(
&mut self,
ctx: &Context,
monitor_index: Option<usize>,
notification: 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>>,
) {
let show_all_icons = render_config.borrow().show_all_icons;
match notification.event {
NotificationEvent::WindowManager(_) => {}
NotificationEvent::Monitor(_) => {}
NotificationEvent::Socket(message) => match message {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color.clone(),
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,
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
},
}
self.monitor_usr_idx_map = notification.state.monitor_usr_idx_map.clone();
if monitor_index.is_none()
|| monitor_index.is_some_and(|idx| idx >= notification.state.monitors.elements().len())
{
// The bar's monitor is diconnected, so the bar is disabled no need to check anything
// any further otherwise we'll get `OutOfBounds` panics.
return;
}
let monitor_index = monitor_index.expect("should have a monitor index");
self.monitor_index = monitor_index;
self.mouse_follows_focus = notification.state.mouse_follows_focus;
let monitor = &notification.state.monitors.elements()[monitor_index];
self.work_area_offset =
notification.state.monitors.elements()[monitor_index].work_area_offset();
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_show = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.is_empty()
} else {
true
};
if should_show {
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
if show_all_icons {
let mut containers = vec![];
let mut has_monocle = false;
// add monocle container
if let Some(container) = ws.monocle_container() {
containers.push((true, container.into()));
has_monocle = true;
}
// add all tiled windows
for (i, container) in ws.containers().iter().enumerate() {
containers.push((
!has_monocle && i == ws.focused_container_idx(),
container.into(),
));
}
// add all floating windows
for floating_window in ws.floating_windows() {
containers.push((
!has_monocle && floating_window.is_focused(),
floating_window.into(),
));
}
containers
} else {
vec![(true, ws.into())]
},
ws.layer().to_owned(),
));
}
}
self.workspaces = workspaces;
if monitor.workspaces()[focused_workspace_idx]
.monocle_container()
.is_some()
{
self.layout = KomorebiLayout::Monocle;
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
} else if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
} else {
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
}
self.focused_container_information = (&monitor.workspaces()[focused_workspace_idx]).into();
}
}
#[derive(Clone, Debug)]
pub struct KomorebiNotificationStateContainerInformation {
pub titles: Vec<String>,
pub icons: Vec<Option<RgbaImage>>,
pub focused_window_idx: usize,
}
impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
fn from(value: &Workspace) -> Self {
let mut container_info = Self::EMPTY;
if let Some(container) = value.monocle_container() {
container_info = container.into();
} else if let Some(container) = value.focused_container() {
container_info = container.into();
}
for floating_window in value.floating_windows() {
if floating_window.is_focused() {
container_info = floating_window.into();
}
}
container_info
}
}
impl From<&Container> for KomorebiNotificationStateContainerInformation {
fn from(value: &Container) -> Self {
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,
focused_window_idx: value.focused_window_idx(),
}
}
}
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,
focused_window_idx: 0,
}
}
}
impl KomorebiNotificationStateContainerInformation {
pub const EMPTY: Self = Self {
titles: vec![],
icons: vec![],
focused_window_idx: 0,
};
}

View File

@@ -1,21 +1,9 @@
mod bar;
mod battery;
mod config;
mod cpu;
mod date;
mod keyboard;
mod komorebi;
mod komorebi_layout;
mod media;
mod memory;
mod network;
mod render;
mod selected_frame;
mod storage;
mod time;
mod ui;
mod update;
mod widget;
mod widgets;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
@@ -27,27 +15,25 @@ use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use image::RgbaImage;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::collections::HashMap;
use komorebi_client::replace_env_in_path;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows_core::BOOL;
@@ -60,8 +46,8 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
pub static DEFAULT_PADDING: f32 = 10.0;
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
#[derive(Parser)]
#[clap(author, about, version)]
@@ -74,6 +60,7 @@ struct Opts {
fonts: bool,
/// Path to a JSON or YAML configuration file
#[clap(short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>,
/// Write an example komorebi.bar.json to disk
#[clap(long)]
@@ -116,7 +103,7 @@ fn process_hwnd() -> Option<isize> {
}
pub enum KomorebiEvent {
Notification(komorebi_client::Notification),
Notification(Box<komorebi_client::Notification>),
Reconnect,
}
@@ -125,16 +112,10 @@ fn main() -> color_eyre::Result<()> {
let opts: Opts = Opts::parse();
#[cfg(feature = "schemars")]
if opts.schema {
let settings = SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
let bar_config = schemars::schema_for!(KomobarConfig);
let schema = serde_json::to_string_pretty(&bar_config)?;
println!("{schema}");
std::process::exit(0);
@@ -149,13 +130,17 @@ fn main() -> color_eyre::Result<()> {
}
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
unsafe {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
unsafe {
std::env::set_var("RUST_LOG", "info");
}
}
tracing::subscriber::set_global_default(
@@ -167,13 +152,14 @@ fn main() -> color_eyre::Result<()> {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
);
home
},
);
@@ -182,7 +168,7 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
println!(
"Example komorebi.bar.json file written to {}",
home_dir.as_path().display()
home_dir.display()
);
std::process::exit(0);
@@ -190,16 +176,11 @@ fn main() -> color_eyre::Result<()> {
let default_config_path = home_dir.join("komorebi.bar.json");
let config_path = opts.config.map_or_else(
|| {
if !default_config_path.is_file() {
None
} else {
Some(default_config_path.clone())
}
},
Option::from,
);
let config_path = opts.config.or_else(|| {
default_config_path
.is_file()
.then_some(default_config_path.clone())
});
let mut config = match config_path {
None => {
@@ -209,17 +190,14 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(&default_config_path, komorebi_bar_json)?;
tracing::info!(
"created example configuration file: {}",
default_config_path.as_path().display()
default_config_path.display()
);
KomobarConfig::read(&default_config_path)?
}
Some(ref config) => {
if !opts.aliases {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
tracing::info!("found configuration file: {}", config.display());
}
KomobarConfig::read(config)?
@@ -238,28 +216,30 @@ fn main() -> color_eyre::Result<()> {
)?)?;
let (usr_monitor_index, work_area_offset) = match &config.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
(monitor_config.index, monitor_config.work_area_offset)
}
MonitorConfigOrIndex::Index(idx) => (*idx, None),
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
None => (0, None),
};
let monitor_index = state
.monitor_usr_idx_map
.get(&usr_monitor_index)
.map_or(usr_monitor_index, |i| *i);
MONITOR_RIGHT.store(
state.monitors.elements()[monitor_index].size().right,
state.monitors.elements()[monitor_index].size.right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[monitor_index].size().top,
state.monitors.elements()[monitor_index].size.top,
Ordering::SeqCst,
);
MONITOR_LEFT.store(
state.monitors.elements()[monitor_index].size().left,
state.monitors.elements()[monitor_index].size.left,
Ordering::SeqCst,
);
@@ -269,11 +249,11 @@ fn main() -> color_eyre::Result<()> {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
x: state.monitors.elements()[monitor_index].size.left as f32,
y: state.monitors.elements()[monitor_index].size.top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[monitor_index].size().right as f32,
x: state.monitors.elements()[monitor_index].size.right as f32,
y: 50.0,
}),
})
@@ -281,14 +261,14 @@ fn main() -> color_eyre::Result<()> {
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
x: state.monitors.elements()[monitor_index].size.left as f32,
y: state.monitors.elements()[monitor_index].size.top as f32,
});
}
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[monitor_index].size().right as f32,
x: state.monitors.elements()[monitor_index].size.right as f32,
y: 50.0,
})
}
@@ -319,10 +299,7 @@ fn main() -> color_eyre::Result<()> {
hotwatch.watch(config_path, move |event| match event.kind {
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
Ok(updated) => {
tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
tracing::info!("configuration file updated: {}", config_path_cl.display());
if let Err(error) = tx_config.send(updated) {
tracing::error!("could not send configuration update to gui: {error}")
@@ -349,7 +326,7 @@ fn main() -> color_eyre::Result<()> {
let ctx_komorebi = cc.egui_ctx.clone();
std::thread::spawn(move || {
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
let subscriber_name = format!("komorebi-bar-{}", random_word::get(random_word::Lang::En));
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
filter_state_changes: true,
@@ -376,7 +353,7 @@ fn main() -> color_eyre::Result<()> {
while komorebi_client::send_message(
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
)
.is_err()
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
@@ -399,7 +376,7 @@ fn main() -> color_eyre::Result<()> {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(Box::new(notification))) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}
@@ -427,5 +404,5 @@ fn main() -> color_eyre::Result<()> {
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
}

View File

@@ -1,376 +0,0 @@
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 num_derive::FromPrimitive;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total data transmitted
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)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
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),
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
impl Network {
fn default_interface(&mut self) {
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
}
}
}
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now();
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
activity.clear();
total_activity.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh(true);
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if self.show_activity {
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
),
Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
),
));
}
if self.show_total_activity {
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
Self::to_pretty_bytes(data.total_received(), 1),
Self::to_pretty_bytes(data.total_transmitted(), 1),
))
}
}
}
}
}
self.last_state_activity.clone_from(&activity);
self.last_state_total_activity.clone_from(&total_activity);
self.last_updated_network_activity = now;
}
(activity, total_activity)
}
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 => (
format!(
"{: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"{: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("{} ", reading.received_text),
reading.transmitted_text,
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"UP: {: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s ", reading.received_text),
format!("\u{2211}UP: {}/s", reading.transmitted_text),
),
},
};
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(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
icon_format.font_id.clone(),
icon_format.color,
100.0,
);
// text
layout_job.append(
&text_down,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
// icon
layout_job.append(
&match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
0.0,
icon_format.clone(),
);
// text
layout_job.append(
&text_up,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
Label::new(layout_job).selectable(false)
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
// widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone();
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 {
render_config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone()));
});
}
}
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();
}
}
}
#[derive(Clone)]
enum NetworkReadingFormat {
Speed = 0,
Total = 1,
}
#[derive(Clone)]
struct NetworkReading {
pub format: NetworkReadingFormat,
pub received_text: String,
pub transmitted_text: String,
}
impl NetworkReading {
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
NetworkReading {
format,
received_text: received,
transmitted_text: transmitted,
}
}
}
#[derive(Debug, FromPrimitive)]
enum DataUnit {
B = 0,
K = 1,
M = 2,
G = 3,
T = 4,
P = 5,
E = 6,
Z = 7,
Y = 8,
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}

View File

@@ -1,3 +1,5 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::bar::Alignment;
use crate::config::KomobarConfig;
use crate::config::MonitorConfigOrIndex;
@@ -11,29 +13,38 @@ use eframe::egui::Margin;
use eframe::egui::Shadow;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use komorebi_client::Colour;
use komorebi_client::Rgb;
use serde::Deserialize;
use serde::Serialize;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "kind")]
/// Grouping
pub enum Grouping {
/// No grouping is applied
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
None,
/// Widgets are grouped as a whole
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
Bar(GroupingConfig),
/// Widgets are grouped by alignment
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
Alignment(GroupingConfig),
/// Widgets are grouped individually
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
Widget(GroupingConfig),
}
#[derive(Clone)]
/// Render configuration
pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize,
@@ -55,6 +66,10 @@ pub struct RenderConfig {
pub icon_font_id: FontId,
/// Show all icons on the workspace section of the Komorebi widget
pub show_all_icons: bool,
/// Background color of the selected frame
pub auto_select_fill: Option<Color32>,
/// Text color of the selected frame
pub auto_select_text: Option<Color32>,
}
pub trait RenderExt {
@@ -84,8 +99,9 @@ impl RenderExt for &KomobarConfig {
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
let monitor_idx = match &self.monitor {
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
MonitorConfigOrIndex::Index(idx) => *idx,
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
};
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
@@ -108,6 +124,10 @@ impl RenderExt for &KomobarConfig {
text_font_id,
icon_font_id,
show_all_icons,
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
}
}
}
@@ -133,6 +153,8 @@ impl RenderConfig {
text_font_id: FontId::default(),
icon_font_id: FontId::default(),
show_all_icons: false,
auto_select_fill: None,
auto_select_text: None,
}
}
@@ -339,7 +361,9 @@ impl RenderConfig {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouping configuration
pub struct GroupingConfig {
/// Styles for the grouping
pub style: Option<GroupingStyle>,
@@ -349,8 +373,11 @@ pub struct GroupingConfig {
pub rounding: Option<RoundingConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouping Style
pub enum GroupingStyle {
/// Default
#[serde(alias = "CtByte")]
Default,
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
@@ -369,10 +396,12 @@ pub enum GroupingStyle {
DefaultWithGlowB0O1S2,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Rounding configuration
pub enum RoundingConfig {
/// All 4 corners are the same
/// All 4 corners are the same
Same(f32),
/// All 4 corners are custom. Order: NW, NE, SW, SE
Individual([f32; 4]),

View File

@@ -1,48 +1,81 @@
use eframe::egui::Color32;
use eframe::egui::CursorIcon;
use eframe::egui::Frame;
use eframe::egui::Margin;
use eframe::egui::Response;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::Ui;
/// Same as SelectableLabel, but supports all content
pub struct SelectableFrame {
selected: bool,
selected_fill: Option<Color32>,
}
impl SelectableFrame {
pub fn new(selected: bool) -> Self {
Self { selected }
Self {
selected,
selected_fill: None,
}
}
pub fn new_auto(selected: bool, selected_fill: Option<Color32>) -> Self {
Self {
selected,
selected_fill,
}
}
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
let Self { selected } = self;
let Self {
selected,
selected_fill,
} = self;
Frame::NONE
.show(ui, |ui| {
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
if ui.is_rect_visible(response.rect) {
// take into account the stroke width
let inner_margin = Margin::symmetric(
ui.style().spacing.button_padding.x as i8,
ui.style().spacing.button_padding.y as i8,
ui.style().spacing.button_padding.x as i8 - 1,
ui.style().spacing.button_padding.y as i8 - 1,
);
if selected
|| response.hovered()
|| response.highlighted()
|| response.has_focus()
{
// since the stroke is drawn inside the frame, we always reserve space for it
if selected && response.hovered() {
let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE
.stroke(visuals.bg_stroke)
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
.corner_radius(visuals.corner_radius)
.fill(selected_fill.unwrap_or(visuals.bg_fill))
.inner_margin(inner_margin)
.show(ui, add_contents);
} else if response.hovered() || response.highlighted() || response.has_focus() {
let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
.corner_radius(visuals.corner_radius)
.fill(visuals.bg_fill)
.inner_margin(inner_margin)
.show(ui, add_contents);
} else if selected {
let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_fill))
.corner_radius(visuals.corner_radius)
.fill(selected_fill.unwrap_or(visuals.bg_fill))
.inner_margin(inner_margin)
.show(ui, add_contents);
} else {
Frame::NONE
.stroke(Stroke::new(1.0, Color32::TRANSPARENT))
.inner_margin(inner_margin)
.show(ui, add_contents);
}

View File

@@ -1,127 +0,0 @@
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;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<StorageConfig> for Storage {
fn from(value: StorageConfig) -> Self {
Self {
enable: value.enable,
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
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(true);
self.last_updated = now;
}
let mut disks = vec![];
for disk in &self.disks {
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
disks.push(match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
})
}
disks.sort();
disks.reverse();
disks
}
}
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
for output in self.output() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::HARD_DRIVES.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("cmd.exe")
.args([
"/C",
"explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{
eprintln!("{}", error)
}
}
});
}
}
}
}

View File

@@ -1,89 +0,0 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::keyboard::Keyboard;
use crate::keyboard::KeyboardConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
use crate::media::MediaConfig;
use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::render::RenderConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use crate::update::Update;
use crate::update::UpdateConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
Date(DateConfig),
Keyboard(KeyboardConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
Memory(MemoryConfig),
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
Update(UpdateConfig),
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
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::Keyboard(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_window.as_ref().is_some_and(|w| w.enable)
|| config
.configuration_switcher
.as_ref()
.is_some_and(|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

@@ -0,0 +1,409 @@
use super::ImageIcon;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::Margin;
use eframe::egui::RichText;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::PathExt;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::path::Path;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use tracing;
use which::which;
/// Minimum interval between consecutive application launches to prevent accidental spamming.
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Applications widget configuration
pub struct ApplicationsConfig {
/// Enables or disables the applications widget.
pub enable: bool,
/// Whether to show the launch command on hover (optional).
/// Could be overridden per application. Defaults to `false` if not set.
pub show_command_on_hover: Option<bool>,
/// Horizontal spacing between application buttons.
pub spacing: Option<f32>,
/// Default display format for all applications (optional).
/// Could be overridden per application. Defaults to `Icon`.
pub display: Option<ApplicationsDisplayFormat>,
/// List of configured applications to display.
pub items: Vec<AppConfig>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Application button configuration
pub struct AppConfig {
/// Whether to enable this application button (optional).
/// Inherits from the global `Applications` setting if omitted.
pub enable: Option<bool>,
/// Whether to show the launch command on hover (optional).
/// Inherits from the global `Applications` setting if omitted.
pub show_command_on_hover: Option<bool>,
/// Display name of the application.
pub name: String,
/// Optional icon: a path to an image or a text-based glyph (e.g., from Nerd Fonts).
/// If not set, and if the `command` is a path to an executable, an icon might be extracted from it.
/// Note: glyphs require a compatible `font_family`.
pub icon: Option<String>,
/// Command to execute (e.g. path to the application or shell command).
pub command: String,
/// Display format for this application button (optional). Overrides global format if set.
pub display: Option<ApplicationsDisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Applications widget display format
pub enum ApplicationsDisplayFormat {
/// Show only the application icon.
#[default]
Icon,
/// Show only the application name as text.
Text,
/// Show both the application icon and name.
IconAndText,
}
#[derive(Clone, Debug)]
pub struct Applications {
/// Whether the applications widget is enabled.
pub enable: bool,
/// Horizontal spacing between application buttons.
pub spacing: Option<f32>,
/// Applications to be rendered in the UI.
pub items: Vec<App>,
}
impl BarWidget for Applications {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if !self.enable {
return;
}
let icon_config = IconConfig {
font_id: config.icon_font_id.clone(),
size: config.icon_font_id.size,
color: ctx.style().visuals.selection.stroke.color,
};
if let Some(spacing) = self.spacing {
ui.spacing_mut().item_spacing.x = spacing;
}
config.apply_on_widget(false, ui, |ui| {
for app in &mut self.items {
app.render(ctx, ui, &icon_config);
}
});
}
}
impl From<&ApplicationsConfig> for Applications {
fn from(applications_config: &ApplicationsConfig) -> Self {
let items = applications_config
.items
.iter()
.enumerate()
.map(|(index, config)| {
let command = UserCommand::new(&config.command);
App {
enable: config.enable.unwrap_or(applications_config.enable),
#[allow(clippy::obfuscated_if_else)]
name: config
.name
.is_empty()
.then(|| format!("App {}", index + 1))
.unwrap_or_else(|| config.name.clone()),
icon: Icon::try_from_path(config.icon.as_deref())
.or_else(|| Icon::try_from_command(&command)),
command,
display: config
.display
.or(applications_config.display)
.unwrap_or_default(),
show_command_on_hover: config
.show_command_on_hover
.or(applications_config.show_command_on_hover)
.unwrap_or(false),
}
})
.collect();
Self {
enable: applications_config.enable,
items,
spacing: applications_config.spacing,
}
}
}
/// A single resolved application entry used at runtime.
#[derive(Clone, Debug)]
pub struct App {
/// Whether this application is enabled.
pub enable: bool,
/// Display name of the application. Defaults to "App N" if not set.
pub name: String,
/// Icon to display for this application, if available.
pub icon: Option<Icon>,
/// Command to execute when the application is launched.
pub command: UserCommand,
/// Display format (icon, text, or both).
pub display: ApplicationsDisplayFormat,
/// Whether to show the launch command on hover.
pub show_command_on_hover: bool,
}
impl App {
/// Renders the application button in the provided `Ui` context with a given icon size.
#[inline]
pub fn render(&mut self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
if self.enable
&& SelectableFrame::new(false)
.show(ui, |ui| {
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
match self.display {
ApplicationsDisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
ApplicationsDisplayFormat::Text => self.draw_name(ui),
ApplicationsDisplayFormat::IconAndText => {
self.draw_icon(ctx, ui, icon_config);
self.draw_name(ui);
}
}
// Add hover text with command information
let response = ui.response();
if self.show_command_on_hover {
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
}
})
.clicked()
{
// Launch the application when clicked
self.command.launch_if_ready();
}
}
/// Draws the application's icon within the UI if available,
/// or falls back to a default placeholder icon.
#[inline]
fn draw_icon(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
if let Some(icon) = &self.icon {
icon.draw(ctx, ui, icon_config);
} else {
Icon::draw_fallback(ui, Vec2::splat(icon_config.size));
}
}
/// Displays the application's name as a non-selectable label within the UI.
#[inline]
fn draw_name(&self, ui: &mut Ui) {
ui.add(Label::new(&self.name).selectable(false));
}
}
/// Holds image/text data to be used as an icon in the UI.
/// This represents source icon data before rendering.
#[derive(Clone, Debug)]
pub enum Icon {
/// RGBA image used for rendering the icon.
Image(ImageIcon),
/// Text-based icon, e.g. from a font like Nerd Fonts.
Text(String),
}
impl Icon {
/// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
///
/// - Environment variables in the path are resolved using [`PathExt::replace_env`].
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved path.
/// - If the path is invalid but the string is non-empty, it is interpreted as a text-based icon and
/// returned as [`Icon::Text`].
/// - Returns `None` if the input is empty, `None`, or image loading fails.
#[inline]
pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
let icon = icon.map(str::trim)?;
if icon.is_empty() {
return None;
}
let path = icon.replace_env();
if !path.is_file() {
return Some(Icon::Text(icon.to_owned()));
}
let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
Ok(img) => Some(img),
Err(err) => {
tracing::error!("Failed to load icon from {:?}, error: {}", path, err);
None
}
})?;
Some(Icon::Image(image_icon))
}
/// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`].
///
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path.
/// - Returns [`Icon::Image`] if an icon is successfully extracted.
/// - Returns `None` if the executable path is unavailable or icon extraction fails.
#[inline]
pub fn try_from_command(command: &UserCommand) -> Option<Self> {
let path = command.get_executable()?;
let image_icon = ImageIcon::try_load(path.as_ref(), || {
let path_str = path.to_str()?;
windows_icons::get_icon_by_path(path_str)
.or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
})?;
Some(Icon::Image(image_icon))
}
/// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
#[inline]
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
match self {
Icon::Image(image_icon) => {
Frame::NONE
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
.show(ui, |ui| {
ui.add(
Image::from_texture(&image_icon.texture(ctx))
.maintain_aspect_ratio(true)
.fit_to_exact_size(Vec2::splat(icon_config.size)),
);
});
}
Icon::Text(icon) => {
let rich_text = RichText::new(icon)
.font(icon_config.font_id.clone())
.size(icon_config.size)
.color(icon_config.color);
ui.add(Label::new(rich_text).selectable(false));
}
}
}
/// Draws a fallback icon when the specified icon cannot be loaded.
/// Displays a simple crossed-out rectangle as a placeholder.
#[inline]
pub fn draw_fallback(ui: &mut Ui, icon_size: Vec2) {
let (response, painter) = ui.allocate_painter(icon_size, Sense::hover());
let stroke = Stroke::new(1.0, ui.style().visuals.text_color());
let mut rect = response.rect;
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
}
}
/// Configuration structure for icon rendering
#[derive(Clone, Debug)]
pub struct IconConfig {
/// Font used for text-based icons
pub font_id: FontId,
/// Size of the icon
pub size: f32,
/// Color of the icon used for text-based icons
pub color: Color32,
}
/// A structure to manage command execution with cooldown prevention.
#[derive(Clone, Debug)]
pub struct UserCommand {
/// The command string to execute
pub command: Arc<str>,
/// Last time this command was executed (used for cooldown control)
pub last_launch: Instant,
}
impl AsRef<str> for UserCommand {
#[inline]
fn as_ref(&self) -> &str {
&self.command
}
}
impl UserCommand {
/// Creates a new [`UserCommand`] with environment variables in the command path
/// resolved using [`PathExt::replace_env`].
#[inline]
pub fn new(command: &str) -> Self {
// Allow immediate launch by initializing last_launch in the past
let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;
Self {
command: Arc::from(command.replace_env().to_str().unwrap_or_default()),
last_launch,
}
}
/// Attempts to resolve the executable path from the command string.
///
/// Resolution logic:
/// - Splits the command by ".exe" and checks if the first part is an existing file.
/// - If not, attempts to locate the binary using [`which`] on this name.
/// - If still unresolved, takes the first word (separated by whitespace) and attempts
/// to find it in the system `PATH` using [`which`].
///
/// Returns `None` if no executable path can be determined.
#[inline]
pub fn get_executable(&self) -> Option<Cow<'_, Path>> {
if let Some(binary) = self.command.split(".exe").next().map(Path::new) {
if binary.is_file() {
return Some(Cow::Borrowed(binary));
} else if let Ok(binary) = which(binary) {
return Some(Cow::Owned(binary));
}
}
which(self.command.split(' ').next()?).ok().map(Cow::Owned)
}
/// Attempts to launch the specified command in a separate thread if enough time has passed
/// since the last launch. This prevents repeated launches from rapid consecutive clicks.
///
/// Errors during launch are logged using the `tracing` crate.
pub fn launch_if_ready(&mut self) {
let now = Instant::now();
// Check if enough time has passed since the last launch
if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL {
return;
}
self.last_launch = now;
let command_string = self.command.clone();
// Launch the application in a separate thread to avoid blocking the UI
std::thread::spawn(move || {
if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() {
tracing::error!("Failed to launch command '{}': {}", command_string, e);
}
});
}
}

View File

@@ -1,33 +1,37 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager;
use starship_battery::State;
use starship_battery::units::ratio::percent;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Battery widget configuration
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
/// Hide the widget if the battery is at full charge
pub hide_on_full_charge: Option<bool>,
/// Data refresh interval (default: 10 seconds)
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is under this value [[1-100]]
pub auto_select_under: Option<u8>,
}
impl From<BatteryConfig> for Battery {
@@ -38,9 +42,10 @@ impl From<BatteryConfig> for Battery {
enable: value.enable,
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
manager: Manager::new().unwrap(),
last_state: String::new(),
last_state: None,
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select_under: value.auto_select_under.map(|u| u.clamp(1, 100)),
state: BatteryState::Discharging,
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
@@ -52,6 +57,16 @@ impl From<BatteryConfig> for Battery {
pub enum BatteryState {
Charging,
Discharging,
High,
Medium,
Low,
Warning,
}
#[derive(Clone, Debug)]
struct BatteryOutput {
label: String,
selected: bool,
}
pub struct Battery {
@@ -61,38 +76,54 @@ pub struct Battery {
pub state: BatteryState,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_state: String,
auto_select_under: Option<u8>,
last_state: Option<BatteryOutput>,
last_updated: Instant,
}
impl Battery {
fn output(&mut self) -> String {
fn output(&mut self) -> Option<BatteryOutput> {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output.clear();
output = None;
if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
if let Ok(mut batteries) = self.manager.batteries()
&& let Some(Ok(first)) = batteries.nth(0)
{
let percentage = first.state_of_charge().get::<percent>().round() as u8;
if percentage == 100.0 && self.hide_on_full_charge {
output = String::new()
} else {
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => self.state = BatteryState::Discharging,
_ => {}
}
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
if percentage == 100 && self.hide_on_full_charge {
output = None
} else {
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => {
self.state = match percentage {
p if p > 75 => BatteryState::Discharging,
p if p > 50 => BatteryState::High,
p if p > 25 => BatteryState::Medium,
p if p > 10 => BatteryState::Low,
_ => BatteryState::Warning,
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
_ => {}
}
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
output = Some(BatteryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage}%")
}
LabelPrefix::None | LabelPrefix::Icon => {
format!("{percentage}%")
}
},
selected,
})
}
}
@@ -108,44 +139,50 @@ impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
if let Some(output) = output {
let emoji = match self.state {
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
BatteryState::High => egui_phosphor::regular::BATTERY_HIGH,
BatteryState::Medium => egui_phosphor::regular::BATTERY_MEDIUM,
BatteryState::Low => egui_phosphor::regular::BATTERY_LOW,
BatteryState::Warning => egui_phosphor::regular::BATTERY_WARNING,
};
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("cmd.exe")
&& let Err(error) = Command::new("cmd.exe")
.args(["/C", "start", "ms-settings:batterysaver"])
.spawn()
{
eprintln!("{}", error)
}
{
eprintln!("{error}")
}
});
}

View File

@@ -1,14 +1,13 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -17,14 +16,19 @@ use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// CPU widget configuration
pub struct CpuConfig {
/// Enable the Cpu widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
}
impl From<CpuConfig> for Cpu {
@@ -38,6 +42,7 @@ impl From<CpuConfig> for Cpu {
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
@@ -45,26 +50,38 @@ impl From<CpuConfig> for Cpu {
}
}
#[derive(Clone, Debug)]
struct CpuOutput {
label: String,
selected: bool,
}
pub struct Cpu {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant,
}
impl Cpu {
fn output(&mut self) -> String {
fn output(&mut self) -> CpuOutput {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage();
self.last_updated = now;
}
let used = self.system.global_cpu_usage();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
let used = self.system.global_cpu_usage() as u8;
let selected = self.auto_select_over.is_some_and(|o| used >= o);
CpuOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
},
selected,
}
}
}
@@ -73,7 +90,9 @@ impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
if !output.label.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -82,31 +101,31 @@ impl BarWidget for Cpu {
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) =
&& let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
{
eprintln!("{error}")
}
});
}

View File

@@ -1,20 +1,25 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use crate::widgets::widget::BarWidget;
use chrono::Local;
use chrono_tz::Tz;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
/// Custom format with additive modifiers for integer format specifiers
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CustomModifiers {
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
format: String,
@@ -38,7 +43,7 @@ impl CustomModifiers {
}
// get the strftime value of modifier
let formatted_modifier = chrono::Local::now().format(modifier).to_string();
let formatted_modifier = Local::now().format(modifier).to_string();
// find the gotten value in the original output
if let Some(pos) = modified_output.find(&formatted_modifier) {
@@ -55,7 +60,9 @@ impl CustomModifiers {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Date widget configuration
pub struct DateConfig {
/// Enable the Date widget
pub enable: bool,
@@ -63,19 +70,42 @@ pub struct DateConfig {
pub format: DateFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
///
/// Use a custom format to display additional information, i.e.:
/// ```json
/// {
/// "Date": {
/// "enable": true,
/// "format": { "Custom": "%D %Z (Tokyo)" },
/// "timezone": "Asia/Tokyo"
/// }
///}
/// ```
pub timezone: Option<String>,
}
impl From<DateConfig> for Date {
fn from(value: DateConfig) -> Self {
let data_refresh_interval = 1;
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
timezone: value.timezone,
data_refresh_interval,
last_state: String::new(),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Date widget format
pub enum DateFormat {
/// Month/Date/Year format (09/08/24)
MonthDateYear,
@@ -86,8 +116,10 @@ pub enum DateFormat {
/// Day Date Month Year format (8 September 2024)
DayDateMonthYear,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
Custom(String),
/// Custom format with modifiers
#[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))]
CustomModifiers(CustomModifiers),
}
@@ -119,19 +151,46 @@ pub struct Date {
pub enable: bool,
pub format: DateFormat,
label_prefix: LabelPrefix,
timezone: Option<String>,
data_refresh_interval: u64,
last_state: String,
last_updated: Instant,
}
impl Date {
fn output(&mut self) -> String {
let formatted = chrono::Local::now()
.format(&self.format.fmt_string())
.to_string();
let mut output = self.last_state.clone();
let now = Instant::now();
// if custom modifiers are used, apply them
match &self.format {
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
_ => formatted,
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
let formatted = match &self.timezone {
Some(timezone) => match timezone.parse::<Tz>() {
Ok(tz) => Local::now()
.with_timezone(&tz)
.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Err(_) => format!("Invalid timezone: {timezone}"),
},
None => Local::now()
.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
};
// if custom modifiers are used, apply them
output = match &self.format {
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
_ => formatted,
};
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
}
@@ -171,7 +230,7 @@ impl BarWidget for Date {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
.selectable(false),
)
})

View File

@@ -1,16 +1,17 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use crate::widgets::widget::BarWidget;
use color_eyre::eyre;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use windows::Win32::Globalization::LCIDToLocaleName;
@@ -20,14 +21,16 @@ use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
const ERROR_TEXT: &str = "Error";
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Keyboard widget configuration
pub struct KeyboardConfig {
/// Enable the Input widget
pub enable: bool,
/// Data refresh interval (default: 1 second)
/// Data refresh interval
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
@@ -35,9 +38,7 @@ pub struct KeyboardConfig {
impl From<KeyboardConfig> for Keyboard {
fn from(value: KeyboardConfig) -> Self {
let data_refresh_interval = value
.data_refresh_interval
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
@@ -80,7 +81,7 @@ pub struct Keyboard {
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
/// invalid UTF-16 characters during conversion.
fn get_active_keyboard_layout() -> Result<String, ()> {
fn get_active_keyboard_layout() -> eyre::Result<String, ()> {
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
@@ -169,7 +170,10 @@ impl BarWidget for Keyboard {
);
config.apply_on_widget(true, ui, |ui| {
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
ui.add(
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
.selectable(false),
)
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
use crate::config::DisplayFormat;
use crate::komorebi::KomorebiLayoutConfig;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use eframe::egui::vec2;
use crate::widgets::komorebi::KomorebiLayoutConfig;
use color_eyre::eyre;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
@@ -13,36 +13,43 @@ use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error;
use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Komorebi layout kind
pub enum KomorebiLayout {
/// Predefined layout
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Default(komorebi_client::DefaultLayout),
/// Monocle mode
Monocle,
/// Floating layer
Floating,
/// Paused
Paused,
/// Custom layout
Custom,
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) =
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
{
return Ok(KomorebiLayout::Default(default_layout));
}
@@ -53,7 +60,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
}
}
}
@@ -92,25 +99,34 @@ impl KomorebiLayout {
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
match self {
KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx {
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
if let Some(ws_idx) = workspace_idx
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
monitor_idx,
ws_idx,
*option,
))
.is_err()
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
}
KomorebiLayout::Monocle => {
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
if komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleMonocle,
])
.is_err()
{
tracing::error!("could not send message to komorebi: ToggleMonocle");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
if komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleTiling,
])
.is_err()
{
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
@@ -178,6 +194,12 @@ impl KomorebiLayout {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
}
// TODO: @CtByte can you think of a nice icon to draw here?
komorebi_client::DefaultLayout::Scrolling => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
},
KomorebiLayout::Monocle => {}
KomorebiLayout::Floating => {
@@ -241,7 +263,7 @@ impl KomorebiLayout {
let layout_frame = SelectableFrame::new(false)
.show(ui, |ui| {
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
self.show_icon(false, font_id.clone(), ctx, ui);
self.show_icon(true, font_id.clone(), ctx, ui);
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
@@ -254,57 +276,53 @@ impl KomorebiLayout {
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
}
if show_options {
if let Some(workspace_idx) = workspace_idx {
Frame::NONE.show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
if show_options && let Some(workspace_idx) = workspace_idx {
Frame::NONE.show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::HorizontalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
for layout_option in &mut layout_options {
let is_selected = self == layout_option;
for layout_option in &mut layout_options {
let is_selected = self == layout_option;
if SelectableFrame::new(is_selected)
.show(ui, |ui| {
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
})
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
}
if SelectableFrame::new(is_selected)
.show(ui, |ui| {
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
})
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
}
});

View File

@@ -1,22 +1,23 @@
use crate::MAX_LABEL_WIDTH;
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 crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget configuration
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
@@ -40,36 +41,34 @@ impl Media {
enable,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap()
.get()
.join()
.unwrap(),
}
}
pub fn toggle(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(op) = session.TryTogglePlayPauseAsync() {
op.get().unwrap_or_default();
}
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(op) = session.TryTogglePlayPauseAsync()
{
op.join().unwrap_or_default();
}
}
fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
if let Ok(properties) = operation.get() {
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
if artist.is_empty() {
return format!("{title}");
}
if title.is_empty() {
return format!("{artist}");
}
return format!("{artist} - {title}");
}
}
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
&& let Ok(properties) = operation.join()
&& let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title())
{
if artist.is_empty() {
return format!("{title}");
}
if title.is_empty() {
return format!("{artist}");
}
return format!("{artist} - {title}");
}
String::new()

View File

@@ -1,14 +1,13 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -17,14 +16,19 @@ use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Memory widget configuration
pub struct MemoryConfig {
/// Enable the Memory widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
}
impl From<MemoryConfig> for Memory {
@@ -38,6 +42,7 @@ impl From<MemoryConfig> for Memory {
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
@@ -45,16 +50,23 @@ impl From<MemoryConfig> for Memory {
}
}
#[derive(Clone, Debug)]
struct MemoryOutput {
label: String,
selected: bool,
}
pub struct Memory {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant,
}
impl Memory {
fn output(&mut self) -> String {
fn output(&mut self) -> MemoryOutput {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_memory();
@@ -63,11 +75,17 @@ impl Memory {
let used = self.system.used_memory();
let total = self.system.total_memory();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
let usage = ((used * 100) / total) as u8;
let selected = self.auto_select_over.is_some_and(|o| usage >= o);
MemoryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {usage}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
},
selected,
}
}
}
@@ -76,7 +94,9 @@ impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
if !output.label.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -85,31 +105,31 @@ impl BarWidget for Memory {
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) =
&& let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
{
eprintln!("{error}")
}
});
}

View File

@@ -0,0 +1,173 @@
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use image::RgbaImage;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::RwLock;
pub mod applications;
pub mod battery;
pub mod cpu;
pub mod date;
pub mod keyboard;
pub mod komorebi;
mod komorebi_layout;
pub mod media;
pub mod memory;
pub mod network;
pub mod storage;
pub mod time;
pub mod update;
pub mod widget;
/// Global cache for icon images and their associated GPU textures.
pub static ICONS_CACHE: IconsCache = IconsCache::new();
/// In-memory cache for icon images and their associated GPU textures.
///
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
#[allow(clippy::type_complexity)]
pub struct IconsCache {
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
}
impl IconsCache {
/// Creates a new empty IconsCache instance.
#[inline]
pub const fn new() -> Self {
Self {
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
images: LazyLock::new(|| RwLock::new(HashMap::new())),
}
}
/// Retrieves or creates a texture handle for the given icon ID and image.
///
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
/// Otherwise, a new texture is created, inserted into the cache, and returned.
/// The cache is reset if the [`Context`] has changed.
#[inline]
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
if let Some(texture) = self.get_texture(ctx, id) {
return texture;
}
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
self.insert_texture(ctx, id.clone(), texture_handle.clone());
texture_handle
}
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
let textures_lock = self.textures.read().unwrap();
if textures_lock.0.as_ref() == Some(ctx) {
return textures_lock.1.get(id).cloned();
}
None
}
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
let mut textures_lock = self.textures.write().unwrap();
if textures_lock.0.as_ref() != Some(ctx) {
textures_lock.0 = Some(ctx.clone());
textures_lock.1.clear();
}
textures_lock.1.insert(id, texture);
}
/// Returns the cached image for the given icon ID, if available.
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
self.images.read().unwrap().get(id).cloned()
}
/// Caches a raw [`ColorImage`] associated with the given icon ID.
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
self.images.write().unwrap().insert(id, image);
}
}
#[inline]
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
}
/// Represents an image-based icon with a unique ID and pixel data.
#[derive(Clone, Debug)]
pub struct ImageIcon {
/// Unique identifier for the image icon, used for texture caching.
pub id: ImageIconId,
/// Shared pixel data of the icon in `ColorImage` format.
pub image: Arc<ColorImage>,
}
impl ImageIcon {
/// Creates a new [`ImageIcon`] from the given ID and image data.
#[inline]
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
Self { id, image }
}
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
#[inline]
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
where
F: FnOnce() -> Option<I>,
I: Into<RgbaImage>,
{
let id = id.into();
let image = ICONS_CACHE.get_image(&id).or_else(|| {
let img = loader()?;
let img = Arc::new(rgba_to_color_image(&img.into()));
ICONS_CACHE.insert_image(id.clone(), img.clone());
Some(img)
})?;
Some(ImageIcon::new(id, image))
}
/// Returns a texture handle for the icon, using the given [`Context`].
///
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
#[inline]
pub fn texture(&self, ctx: &Context) -> TextureHandle {
ICONS_CACHE.texture(ctx, &self.id, &self.image)
}
}
/// Unique identifier for an image-based icon.
///
/// Used to distinguish cached images and textures by either a file path
/// or a Windows window handle.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ImageIconId {
/// Identifier based on a file system path.
Path(Arc<Path>),
/// Windows HWND handle.
Hwnd(isize),
}
impl From<&Path> for ImageIconId {
#[inline]
fn from(value: &Path) -> Self {
Self::Path(value.into())
}
}
impl From<isize> for ImageIconId {
#[inline]
fn from(value: isize) -> Self {
Self::Hwnd(value)
}
}

View File

@@ -0,0 +1,613 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use num_derive::FromPrimitive;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network widget configuration
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total received and transmitted activity
#[serde(alias = "show_total_data_transmitted")]
pub show_total_activity: bool,
/// Show received and transmitted activity
#[serde(alias = "show_network_activity")]
pub show_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Characters to reserve for received and transmitted activity
#[serde(alias = "network_activity_fill_characters")]
pub activity_left_padding: Option<usize>,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
pub auto_select: Option<NetworkSelectConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network select configuration
pub struct NetworkSelectConfig {
/// Select the total received data when it's over this value
pub total_received_over: Option<u64>,
/// Select the total transmitted data when it's over this value
pub total_transmitted_over: Option<u64>,
/// Select the received data when it's over this value
pub received_over: Option<u64>,
/// Select the transmitted data when it's over this value
pub transmitted_over: Option<u64>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let default_refresh_interval = 10;
let data_refresh_interval = value
.data_refresh_interval
.unwrap_or(default_refresh_interval);
Self {
enable: value.enable,
show_total_activity: value.show_total_activity,
show_activity: value.show_activity,
show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Arc::new(Mutex::new(Networks::new_with_refreshed_list())),
default_interface: Arc::new(Mutex::new(String::new())),
interface_generation: Arc::new(AtomicU64::new(0)),
default_refresh_interval,
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select: value.auto_select,
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
last_update_request_default_interface: Instant::now()
.checked_sub(Duration::from_secs(default_refresh_interval))
.unwrap(),
last_state_total_activity: Arc::new(Mutex::new(vec![])),
last_state_activity: Arc::new(Mutex::new(vec![])),
last_update_request_network_activity: Arc::new(Mutex::new(
Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
)),
activity_generation: Arc::new(AtomicU64::new(0)),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Arc<Mutex<Networks>>,
default_refresh_interval: u64,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select: Option<NetworkSelectConfig>,
default_interface: Arc<Mutex<String>>,
interface_generation: Arc<AtomicU64>,
last_update_request_default_interface: Instant,
activity_generation: Arc<AtomicU64>,
last_state_activity: Arc<Mutex<Vec<NetworkReading>>>,
last_state_total_activity: Arc<Mutex<Vec<NetworkReading>>>,
last_update_request_network_activity: Arc<Mutex<Instant>>,
activity_left_padding: usize,
}
impl Network {
fn update_default_interface_async(&mut self) {
let gen_ = self.interface_generation.fetch_add(1, Ordering::SeqCst) + 1;
let gen_arc = Arc::clone(&self.interface_generation);
let iface_arc = Arc::clone(&self.default_interface);
thread::spawn(move || {
if let Ok(interface) = netdev::get_default_interface()
&& let Some(friendly_name) = &interface.friendly_name
{
// Only update if this is the latest request
if gen_ == gen_arc.load(Ordering::SeqCst)
&& let Ok(mut iface) = iface_arc.lock()
{
*iface = friendly_name.clone();
}
}
});
}
fn default_interface(&mut self) -> String {
let current = self.default_interface.lock().unwrap().clone();
let now = Instant::now();
if now.duration_since(self.last_update_request_default_interface)
> Duration::from_secs(self.default_refresh_interval)
{
self.last_update_request_default_interface = now;
self.update_default_interface_async();
}
current
}
fn update_network_activity_async(&mut self) {
let gen_ = self.activity_generation.fetch_add(1, Ordering::SeqCst) + 1;
let gen_arc = Arc::clone(&self.activity_generation);
let activity_arc = Arc::clone(&self.last_state_activity);
let total_activity_arc = Arc::clone(&self.last_state_total_activity);
let data_refresh_interval = self.data_refresh_interval;
let show_activity = self.show_activity;
let show_total_activity = self.show_total_activity;
let networks_network_activity_arc = Arc::clone(&self.networks_network_activity);
thread::spawn(move || {
let mut activity = Vec::new();
let mut total_activity = Vec::new();
if let Ok(interface) = netdev::get_default_interface()
&& let Some(friendly_name) = &interface.friendly_name
&& let Ok(mut networks) = networks_network_activity_arc.lock()
{
networks.refresh(true);
for (interface_name, data) in &*networks {
if friendly_name.eq(interface_name) {
if show_activity {
let received =
Network::to_pretty_bytes(data.received(), data_refresh_interval);
let transmitted =
Network::to_pretty_bytes(data.transmitted(), data_refresh_interval);
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
ReadingValue::from(received),
ReadingValue::from(transmitted),
));
}
if show_total_activity {
let total_received = Network::to_pretty_bytes(data.total_received(), 1);
let total_transmitted =
Network::to_pretty_bytes(data.total_transmitted(), 1);
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
ReadingValue::from(total_received),
ReadingValue::from(total_transmitted),
));
}
}
}
}
// Only update if this is the latest request
if gen_ == gen_arc.load(Ordering::SeqCst) {
if let Ok(mut act) = activity_arc.lock() {
*act = activity;
}
if let Ok(mut tot) = total_activity_arc.lock() {
*tot = total_activity;
}
}
});
}
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let now = Instant::now();
let should_update = {
let last_update_request = self.last_update_request_network_activity.lock().unwrap();
now.duration_since(*last_update_request)
> Duration::from_secs(self.data_refresh_interval)
};
if should_update {
{
let mut last_updated = self.last_update_request_network_activity.lock().unwrap();
*last_updated = now;
}
self.update_network_activity_async();
}
self.get_network_activity()
}
fn get_network_activity(&self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let activity = self.last_state_activity.lock().unwrap().clone();
let total_activity = self.last_state_total_activity.lock().unwrap().clone();
(activity, total_activity)
}
fn reading_to_labels(
&self,
select_received: bool,
select_transmitted: bool,
ctx: &Context,
reading: &NetworkReading,
config: RenderConfig,
) -> (Label, Label) {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"{: >width$}/s ",
reading.received.pretty,
width = self.activity_left_padding
),
format!(
"{: >width$}/s",
reading.transmitted.pretty,
width = self.activity_left_padding
),
),
NetworkReadingFormat::Total => (
format!("{} ", reading.received.pretty),
reading.transmitted.pretty.clone(),
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s ",
reading.received.pretty,
width = self.activity_left_padding
),
format!(
"UP: {: >width$}/s",
reading.transmitted.pretty,
width = self.activity_left_padding
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s ", reading.received.pretty),
format!("\u{2211}UP: {}/s", reading.transmitted.pretty),
),
},
};
let auto_text_color_received = config.auto_select_text.filter(|_| select_received);
let auto_text_color_transmitted = config.auto_select_text.filter(|_| select_transmitted);
// icon
let mut layout_job_down = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
if select_received {
egui_phosphor::regular::ARROW_FAT_LINES_DOWN.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
auto_text_color_received.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
// text
layout_job_down.append(
&text_down,
ctx.style().spacing.item_spacing.x,
TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color_received.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
// icon
let mut layout_job_up = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
if select_transmitted {
egui_phosphor::regular::ARROW_FAT_LINES_UP.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
auto_text_color_transmitted.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
// text
layout_job_up.append(
&text_up,
ctx.style().spacing.item_spacing.x,
TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color_transmitted.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
(
Label::new(layout_job_down).selectable(false),
Label::new(layout_job_up).selectable(false),
)
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> (u64, String) {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
(
input as u64,
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
},
)
}
fn show_frame<R>(
&self,
selected: bool,
auto_focus_fill: Option<Color32>,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) {
if SelectableFrame::new_auto(selected, auto_focus_fill)
.show(ui, add_contents)
.clicked()
&& let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{error}");
}
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
// widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone();
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 {
render_config.apply_on_widget(false, ui, |ui| {
let select_received = self.auto_select.is_some_and(|f| {
f.total_received_over
.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.total_transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
});
}
}
if self.show_activity {
for reading in &activity {
render_config.apply_on_widget(false, ui, |ui| {
let select_received = self.auto_select.is_some_and(|f| {
f.received_over.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
});
}
}
}
if self.show_default_interface {
let mut self_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| {
self.show_frame(false, None, ui, |ui| {
ui.add(Label::new(layout_job).selectable(false))
});
});
}
}
// widget spacing: pass on the config that was use for calling the apply_on_widget function
*config = render_config.clone();
}
}
}
#[derive(Clone)]
enum NetworkReadingFormat {
Speed = 0,
Total = 1,
}
#[derive(Clone)]
struct ReadingValue {
value: u64,
pretty: String,
}
impl From<(u64, String)> for ReadingValue {
fn from(value: (u64, String)) -> Self {
Self {
value: value.0,
pretty: value.1,
}
}
}
#[derive(Clone)]
struct NetworkReading {
format: NetworkReadingFormat,
received: ReadingValue,
transmitted: ReadingValue,
}
impl NetworkReading {
fn new(
format: NetworkReadingFormat,
received: ReadingValue,
transmitted: ReadingValue,
) -> Self {
Self {
format,
received,
transmitted,
}
}
}
#[derive(Debug, FromPrimitive)]
enum DataUnit {
B = 0,
K = 1,
M = 2,
G = 3,
T = 4,
P = 5,
E = 6,
Z = 7,
Y = 8,
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
}
}

View File

@@ -0,0 +1,177 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Storage widget configuration
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Show disks that are read only
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
pub show_read_only_disks: Option<bool>,
/// Show removable disks
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub show_removable_disks: Option<bool>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
/// Hide when the current percentage is under this value [[1-100]]
pub auto_hide_under: Option<u8>,
}
impl From<StorageConfig> for Storage {
fn from(value: StorageConfig) -> Self {
Self {
enable: value.enable,
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
show_removable_disks: value.show_removable_disks.unwrap_or(true),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
last_updated: Instant::now(),
}
}
}
struct StorageDisk {
label: String,
selected: bool,
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
show_read_only_disks: bool,
show_removable_disks: bool,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant,
}
impl Storage {
fn output(&mut self) -> Vec<StorageDisk> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh(true);
self.last_updated = now;
}
let mut disks = vec![];
for disk in &self.disks {
if disk.is_read_only() && !self.show_read_only_disks {
continue;
}
if disk.is_removable() && !self.show_removable_disks {
continue;
}
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
let percentage = ((used * 100) / total) as u8;
let hide = self.auto_hide_under.is_some_and(|u| percentage <= u);
if !hide {
let selected = self.auto_select_over.is_some_and(|o| percentage >= o);
disks.push(StorageDisk {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
},
selected,
})
}
}
disks.sort_by(|a, b| a.label.cmp(&b.label));
disks
}
}
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let mut output = self.output();
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
if is_reversed {
output.reverse();
}
for output in output {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::HARD_DRIVES.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.label.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{
eprintln!("{error}")
}
});
}
}
}
}

View File

@@ -2,8 +2,10 @@ use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use crate::widgets::widget::BarWidget;
use chrono::Local;
use chrono::NaiveTime;
use chrono_tz::Tz;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
@@ -13,12 +15,64 @@ use eframe::egui::Stroke;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use eframe::epaint::StrokeKind;
use schemars::JsonSchema;
use lazy_static::lazy_static;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
use std::time::Instant;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
lazy_static! {
static ref TIME_RANGES: Vec<(&'static str, NaiveTime)> = {
vec![
(
egui_phosphor::regular::MOON,
NaiveTime::from_hms_opt(0, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::ALARM,
NaiveTime::from_hms_opt(6, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::BREAD,
NaiveTime::from_hms_opt(6, 1, 0).expect("invalid"),
),
(
egui_phosphor::regular::BARBELL,
NaiveTime::from_hms_opt(6, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::COFFEE,
NaiveTime::from_hms_opt(8, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::CLOCK,
NaiveTime::from_hms_opt(8, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::HAMBURGER,
NaiveTime::from_hms_opt(12, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::CLOCK_AFTERNOON,
NaiveTime::from_hms_opt(12, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::FORK_KNIFE,
NaiveTime::from_hms_opt(18, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::MOON_STARS,
NaiveTime::from_hms_opt(18, 30, 0).expect("invalid"),
),
]
};
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time widget configuration
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
@@ -26,19 +80,47 @@ pub struct TimeConfig {
pub format: TimeFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
///
/// Use a custom format to display additional information, i.e.:
/// ```json
/// {
/// "Time": {
/// "enable": true,
/// "format": { "Custom": "%T %Z (Tokyo)" },
/// "timezone": "Asia/Tokyo"
/// }
///}
/// ```
pub timezone: Option<String>,
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00
pub changing_icon: Option<bool>,
}
impl From<TimeConfig> for Time {
fn from(value: TimeConfig) -> Self {
// using 1 second made the widget look "less accurate" and lagging (especially having multiple with seconds).
// This is still better than getting an update every frame
let data_refresh_interval = 500;
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
timezone: value.timezone,
changing_icon: value.changing_icon.unwrap_or_default(),
data_refresh_interval_millis: data_refresh_interval,
last_state: TimeOutput::new(),
last_updated: Instant::now()
.checked_sub(Duration::from_millis(data_refresh_interval))
.unwrap(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time format
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
@@ -53,6 +135,7 @@ pub enum TimeFormat {
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
BinaryRectangle,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
Custom(String),
}
@@ -82,20 +165,94 @@ impl TimeFormat {
}
}
#[derive(Clone, Debug)]
struct TimeOutput {
label: String,
icon: String,
}
impl TimeOutput {
fn new() -> Self {
Self {
label: String::new(),
icon: String::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
label_prefix: LabelPrefix,
timezone: Option<String>,
changing_icon: bool,
data_refresh_interval_millis: u64,
last_state: TimeOutput,
last_updated: Instant,
}
impl Time {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string()
fn output(&mut self) -> TimeOutput {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated)
> Duration::from_millis(self.data_refresh_interval_millis)
{
let (formatted, current_time) = match &self.timezone {
Some(timezone) => match timezone.parse::<Tz>() {
Ok(tz) => {
let dt = Local::now().with_timezone(&tz);
(
dt.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Some(dt.time()),
)
}
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
},
None => {
let dt = Local::now();
(
dt.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Some(dt.time()),
)
}
};
if current_time.is_none() {
return TimeOutput {
label: formatted,
icon: egui_phosphor::regular::WARNING_CIRCLE.to_string(),
};
}
let current_range = match &self.changing_icon {
true => TIME_RANGES
.iter()
.rev()
.find(|&(_, start)| current_time.unwrap() > *start)
.cloned(),
false => None,
}
.unwrap_or((egui_phosphor::regular::CLOCK, NaiveTime::default()));
output = TimeOutput {
label: formatted,
icon: current_range.0.to_string(),
};
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
fn paint_binary_circle(
@@ -109,36 +266,37 @@ impl Time {
let full_height = size;
let height = full_height / 4.0;
let width = height;
let offset = height / 2.0 - height / 8.0;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), Sense::hover());
let color = ctx.style().visuals.text_color();
let c = response.rect.center();
let r = height / 2.0 - 0.5;
if number == 1 || number == 3 || number == 5 || number == 7 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, height * 1.50 + offset), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, height * 1.50 + offset), r / 2.5, color);
}
if number == 2 || number == 3 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r / 2.5, color);
}
if number == 4 || number == 5 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r, color);
} else if max_power > 2 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r / 2.5, color);
}
if number == 8 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), r, color);
} else if max_power > 3 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), r / 2.5, color);
}
}
@@ -153,9 +311,10 @@ impl Time {
let full_height = size;
let height = full_height / 4.0;
let width = height * 1.5;
let offset = height / 2.0 - height / 8.0;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), Sense::hover());
let color = ctx.style().visuals.text_color();
let stroke = Stroke::new(1.0, color);
@@ -173,22 +332,25 @@ impl Time {
};
if max_power == 2 {
let mut rect = response.rect.shrink(stroke.width);
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect.set_height(rect.height() - height * 2.0);
rect = rect.translate(Vec2::new(0.0, height * 2.0));
rect = rect.translate(Vec2::new(0.0, height * 2.0 + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
} else if max_power == 3 {
let mut rect = response.rect.shrink(stroke.width);
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect.set_height(rect.height() - height);
rect = rect.translate(Vec2::new(0.0, height));
rect = rect.translate(Vec2::new(0.0, height + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
} else {
painter.rect_stroke(
response.rect.shrink(stroke.width),
round_all,
stroke,
StrokeKind::Outside,
);
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect = rect.translate(Vec2::new(0.0, 0.0 + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
}
let mut rect_bin = response.rect;
@@ -197,7 +359,7 @@ impl Time {
if number == 1 || number == 5 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 3.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 3.0 + offset * 2.0)),
round_bottom,
color,
);
@@ -205,7 +367,7 @@ impl Time {
if number == 2 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 2.0 + offset * 2.0)),
if max_power == 2 {
round_top
} else {
@@ -217,7 +379,7 @@ impl Time {
if number == 3 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 2.0 + offset * 2.0)),
round_bottom,
color,
);
@@ -225,7 +387,7 @@ impl Time {
if number == 4 || number == 5 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 1.0 + offset * 2.0)),
if max_power == 3 {
round_top
} else {
@@ -237,7 +399,7 @@ impl Time {
if number == 6 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 1.0 + offset * 2.0)),
if max_power == 3 {
round_top
} else {
@@ -249,7 +411,7 @@ impl Time {
if number == 7 {
rect_bin.set_height(height * 3.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height)),
rect_bin.translate(Vec2::new(stroke.width, height + offset * 2.0)),
if max_power == 3 {
round_all
} else {
@@ -260,7 +422,11 @@ impl Time {
}
if number == 8 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(rect_bin.translate(Vec2::new(0.0, 0.0)), round_top, color);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, 0.0 + offset * 2.0)),
round_top,
color,
);
}
}
}
@@ -269,15 +435,13 @@ impl BarWidget for Time {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let use_binary_circle = output.starts_with('c');
let use_binary_rectangle = output.starts_with('r');
if !output.label.is_empty() {
let use_binary_circle = output.label.starts_with('c');
let use_binary_rectangle = output.label.starts_with('r');
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CLOCK.to_string()
}
LabelPrefix::Icon | LabelPrefix::IconAndText => output.icon,
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
@@ -286,12 +450,12 @@ impl BarWidget for Time {
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "TIME: ");
output.label.insert_str(0, "TIME: ");
}
if !use_binary_circle && !use_binary_rectangle {
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
@@ -314,9 +478,9 @@ impl BarWidget for Time {
if use_binary_circle || use_binary_rectangle {
let ordered_output = if is_reversed {
output.chars().rev().collect()
output.label.chars().rev().collect()
} else {
output
output.label
};
for (section_index, section) in

View File

@@ -1,25 +1,27 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use eframe::egui::text::LayoutJob;
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)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Update widget configuration
pub struct UpdateConfig {
/// Enable the Update widget
pub enable: bool,
/// Data refresh interval (default: 12 hours)
/// Data refresh interval in hours
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
@@ -140,16 +142,14 @@ impl BarWidget for Update {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("explorer.exe")
&& let Err(error) = Command::new("explorer.exe")
.args([format!(
"https://github.com/LGUG2Z/komorebi/releases/v{}",
self.latest_version
)])
.spawn()
{
eprintln!("{}", error)
}
{
eprintln!("{error}")
}
});
}

View File

@@ -0,0 +1,119 @@
use crate::render::RenderConfig;
use crate::widgets::applications::Applications;
use crate::widgets::applications::ApplicationsConfig;
use crate::widgets::battery::Battery;
use crate::widgets::battery::BatteryConfig;
use crate::widgets::cpu::Cpu;
use crate::widgets::cpu::CpuConfig;
use crate::widgets::date::Date;
use crate::widgets::date::DateConfig;
use crate::widgets::keyboard::Keyboard;
use crate::widgets::keyboard::KeyboardConfig;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::KomorebiConfig;
use crate::widgets::media::Media;
use crate::widgets::media::MediaConfig;
use crate::widgets::memory::Memory;
use crate::widgets::memory::MemoryConfig;
use crate::widgets::network::Network;
use crate::widgets::network::NetworkConfig;
use crate::widgets::storage::Storage;
use crate::widgets::storage::StorageConfig;
use crate::widgets::time::Time;
use crate::widgets::time::TimeConfig;
use crate::widgets::update::Update;
use crate::widgets::update::UpdateConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Widget configuration
pub enum WidgetConfig {
/// Applications widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
Applications(ApplicationsConfig),
/// Battery widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
Battery(BatteryConfig),
/// CPU widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
Cpu(CpuConfig),
/// Date widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
Date(DateConfig),
/// Keyboard widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Keyboard"))]
Keyboard(KeyboardConfig),
/// Komorebi widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
Komorebi(KomorebiConfig),
/// Media widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Media"))]
Media(MediaConfig),
/// Memory widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
Memory(MemoryConfig),
/// Network widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
Network(NetworkConfig),
/// Storage widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
Storage(StorageConfig),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig),
/// Update widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
Update(UpdateConfig),
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Applications(config) => Box::new(Applications::from(config)),
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
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::Applications(config) => config.enable,
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Keyboard(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_container.as_ref().is_some_and(|w| w.enable)
|| config
.configuration_switcher
.as_ref()
.is_some_and(|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,12 +1,16 @@
[package]
name = "komorebi-client"
version = "0.1.35"
edition = "2021"
version = "0.1.40"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi = { path = "../komorebi" }
komorebi = { path = "../komorebi", default-features = false }
uds_windows = { workspace = true }
serde_json = { workspace = true }
[features]
default = ["schemars"]
schemars = ["komorebi/default"]

View File

@@ -1,19 +1,44 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::AnimationsConfig;
pub use komorebi::AppSpecificConfigurationPath;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::Colour;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GridLayoutOptions;
pub use komorebi::KomorebiTheme;
pub use komorebi::LayoutOptions;
pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::Placement;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::Rgb;
pub use komorebi::RuleDebug;
pub use komorebi::ScrollingLayoutOptions;
pub use komorebi::StackbarConfig;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi::ThemeOptions;
pub use komorebi::VirtualDesktopNotification;
pub use komorebi::Wallpaper;
pub use komorebi::WindowContainerBehaviour;
pub use komorebi::WindowHandlingBehaviour;
pub use komorebi::WindowsApi;
pub use komorebi::WorkspaceConfig;
pub use komorebi::animation::PerAnimationPrefixConfig;
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::border_manager::BorderInfo;
pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::config_generation::IdWithIdentifier;
pub use komorebi::config_generation::IdWithIdentifierAndComment;
pub use komorebi::config_generation::MatchingRule;
pub use komorebi::config_generation::MatchingStrategy;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement;
@@ -28,6 +53,7 @@ pub use komorebi::core::CustomLayout;
pub use komorebi::core::CycleDirection;
pub use komorebi::core::DefaultLayout;
pub use komorebi::core::Direction;
pub use komorebi::core::FloatingLayerBehaviour;
pub use komorebi::core::FocusFollowsMouseImplementation;
pub use komorebi::core::HidingBehaviour;
pub use komorebi::core::Layout;
@@ -42,36 +68,24 @@ pub use komorebi::core::StackbarLabel;
pub use komorebi::core::StackbarMode;
pub use komorebi::core::StateQuery;
pub use komorebi::core::WindowKind;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::replace_env_in_path;
pub use komorebi::monitor::Monitor;
pub use komorebi::monitor_reconciliator::MonitorNotification;
pub use komorebi::ring::Ring;
pub use komorebi::splash;
pub use komorebi::state::GlobalState;
pub use komorebi::state::State;
pub use komorebi::win32_display_data;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::workspace::WorkspaceGlobals;
pub use komorebi::workspace::WorkspaceLayer;
pub use komorebi::AnimationsConfig;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi::WindowContainerBehaviour;
pub use komorebi::WindowsApi;
pub use komorebi::WorkspaceConfig;
use komorebi::DATA_DIR;
use std::borrow::Borrow;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
@@ -89,12 +103,15 @@ 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<()> {
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
where
Q: Borrow<SocketMessage>,
{
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) {
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
s.push_str(&m_str);
s.push('\n');
}

View File

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

View File

@@ -1,9 +1,9 @@
#![warn(clippy::all)]
use eframe::egui;
use eframe::egui::color_picker::Alpha;
use eframe::egui::Color32;
use eframe::egui::ViewportBuilder;
use eframe::egui::color_picker::Alpha;
use komorebi_client::BorderStyle;
use komorebi_client::Colour;
use komorebi_client::DefaultLayout;
@@ -41,7 +41,9 @@ struct BorderColours {
single: Color32,
stack: Color32,
monocle: Color32,
floating: Color32,
unfocused: Color32,
unfocused_locked: Color32,
}
struct BorderConfig {
@@ -76,8 +78,8 @@ impl From<&komorebi_client::Monitor> for MonitorConfig {
}
Self {
size: *value.size(),
work_area_offset: value.work_area_offset().unwrap_or_default(),
size: value.size,
work_area_offset: value.work_area_offset.unwrap_or_default(),
workspaces,
}
}
@@ -93,22 +95,22 @@ struct WorkspaceConfig {
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
fn from(value: &komorebi_client::Workspace) -> Self {
let layout = match value.layout() {
Layout::Default(layout) => *layout,
let layout = match value.layout {
Layout::Default(layout) => layout,
Layout::Custom(_) => DefaultLayout::BSP,
};
let name = value
.name()
.name
.to_owned()
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
Self {
layout,
name,
tile: *value.tile(),
workspace_padding: value.workspace_padding().unwrap_or(20),
container_padding: value.container_padding().unwrap_or(20),
tile: value.tile,
workspace_padding: value.workspace_padding.unwrap_or(20),
container_padding: value.container_padding.unwrap_or(20),
}
}
}
@@ -154,7 +156,9 @@ impl KomorebiGui {
single: colour32(global_state.border_colours.single),
stack: colour32(global_state.border_colours.stack),
monocle: colour32(global_state.border_colours.monocle),
floating: colour32(global_state.border_colours.floating),
unfocused: colour32(global_state.border_colours.unfocused),
unfocused_locked: colour32(global_state.border_colours.unfocused_locked),
};
let border_config = BorderConfig {
@@ -243,7 +247,7 @@ impl eframe::App for KomorebiGui {
egui::CentralPanel::default().show(ctx, |ui| {
ctx.set_pixels_per_point(2.0);
egui::ScrollArea::vertical().show(ui, |ui| {
ui.set_width(ctx.screen_rect().width());
ui.set_width(ctx.content_rect().width());
ui.collapsing("Debugging", |ui| {
ui.collapsing("Window Rules", |ui| {
let window = Window::from(self.debug_hwnd);
@@ -377,6 +381,22 @@ impl eframe::App for KomorebiGui {
}
});
ui.collapsing("Floating", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.floating,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Floating,
self.border_config.border_colours.floating.r() as u32,
self.border_config.border_colours.floating.g() as u32,
self.border_config.border_colours.floating.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Unfocused", |ui| {
if egui::color_picker::color_picker_color32(
ui,
@@ -391,6 +411,22 @@ impl eframe::App for KomorebiGui {
))
.unwrap();
}
});
ui.collapsing("Unfocused Locked", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.unfocused_locked,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::UnfocusedLocked,
self.border_config.border_colours.unfocused_locked.r() as u32,
self.border_config.border_colours.unfocused_locked.g() as u32,
self.border_config.border_colours.unfocused_locked.b() as u32,
))
.unwrap();
}
})
});
@@ -401,7 +437,7 @@ impl eframe::App for KomorebiGui {
BorderStyle::Square,
] {
if ui
.add(egui::SelectableLabel::new(
.add(egui::Button::selectable(
self.border_config.border_style == option,
option.to_string(),
))
@@ -458,7 +494,7 @@ impl eframe::App for KomorebiGui {
StackbarMode::Always,
] {
if ui
.add(egui::SelectableLabel::new(
.add(egui::Button::selectable(
self.stackbar_config.mode == option,
option.to_string(),
))
@@ -477,7 +513,7 @@ impl eframe::App for KomorebiGui {
ui.collapsing("Label", |ui| {
for option in [StackbarLabel::Process, StackbarLabel::Title] {
if ui
.add(egui::SelectableLabel::new(
.add(egui::Button::selectable(
self.stackbar_config.label == option,
option.to_string(),
))
@@ -736,7 +772,7 @@ impl eframe::App for KomorebiGui {
DefaultLayout::Grid,
] {
if ui
.add(egui::SelectableLabel::new(
.add(egui::Button::selectable(
workspace.layout == option,
option.to_string(),
))

View File

@@ -0,0 +1,11 @@
[package]
name = "komorebi-shortcuts"
version = "0.1.0"
edition = "2024"
[dependencies]
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
eframe = { workspace = true }
dirs = { workspace = true }

View File

@@ -0,0 +1,98 @@
use eframe::egui::ViewportBuilder;
use std::path::PathBuf;
use whkd_core::Whkdrc;
#[derive(Default)]
struct Quicklook {
whkdrc: Option<Whkdrc>,
filter: String,
}
impl Quicklook {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let mut home = std::env::var("WHKD_CONFIG_HOME").map_or_else(
|_| {
dirs::home_dir()
.expect("no home directory found")
.join(".config")
},
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
);
}
},
);
home.push("whkdrc");
Self {
whkdrc: whkd_parser::load(&home).ok(),
filter: String::new(),
}
}
}
impl eframe::App for Quicklook {
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
eframe::egui::CentralPanel::default().show(ctx, |ui| {
ui.set_max_width(ui.available_width());
ui.set_max_height(ui.available_height());
eframe::egui::ScrollArea::vertical().show(ui, |ui| {
eframe::egui::Grid::new("grid")
.num_columns(2)
.striped(true)
.spacing([40.0, 4.0])
.min_col_width(ui.available_width() / 2.0 - 20.0)
.show(ui, |ui| {
if let Some(whkdrc) = &self.whkdrc {
ui.label("Filter");
ui.add(
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
.hint_text("Filter by command...")
.background_color(ctx.style().visuals.faint_bg_color),
);
ui.end_row();
for binding in &whkdrc.bindings {
let keys = binding.keys.join(" + ");
if self.filter.is_empty() || binding.command.contains(&self.filter)
{
ui.label(keys);
ui.label(&binding.command);
ui.end_row();
}
}
}
});
});
});
}
}
fn main() {
let viewport_builder = ViewportBuilder::default()
.with_resizable(true)
.with_decorations(false);
let native_options = eframe::NativeOptions {
viewport: viewport_builder,
centered: true,
..Default::default()
};
eframe::run_native(
"komorebi-shortcuts",
native_options,
Box::new(|cc| Ok(Box::new(Quicklook::new(cc)))),
)
.unwrap();
}

View File

@@ -1,14 +1,22 @@
[package]
name = "komorebi-themes"
version = "0.1.35"
edition = "2021"
version = "0.1.40"
edition = "2024"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "b9e26b31f7a0e7ed239b14e5317e95d1bdc544bd" }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
"egui33",
] }
eframe = { workspace = true }
schemars = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_variant = "0.1"
strum = { workspace = true }
hex_color = { version = "3", features = ["serde"] }
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
[features]
default = ["schemars"]
schemars = ["dep:schemars"]

View File

@@ -1,15 +1,17 @@
use hex_color::HexColor;
use komorebi_themes::Color32;
use schemars::gen::SchemaGenerator;
use schemars::schema::InstanceType;
use schemars::schema::Schema;
use schemars::schema::SchemaObject;
use schemars::JsonSchema;
#[cfg(feature = "schemars")]
use schemars::Schema;
#[cfg(feature = "schemars")]
use schemars::SchemaGenerator;
use crate::Color32;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Colour representation
pub enum Colour {
/// Colour represented as RGB
Rgb(Rgb),
@@ -51,21 +53,22 @@ impl From<Colour> for Color32 {
}
}
/// Colour represented as a Hex string
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hex(HexColor);
pub struct Hex(pub HexColor);
impl JsonSchema for Hex {
fn schema_name() -> String {
String::from("Hex")
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Hex {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("Hex")
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("color-hex".to_string()),
..Default::default()
}
.into()
schemars::json_schema!({
"type": "string",
"format": "color-hex",
"description": "Colour represented as a Hex string"
})
}
}
@@ -78,7 +81,9 @@ impl From<Colour> for u32 {
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Colour represented as RGB
pub struct Rgb {
/// Red
pub r: u32,

View File

@@ -0,0 +1,80 @@
use crate::Base16ColourPalette;
use crate::colour::Colour;
use crate::colour::Hex;
use hex_color::HexColor;
use std::collections::VecDeque;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Theme variant
pub enum ThemeVariant {
#[default]
/// Dark variant
Dark,
/// Light variant
Light,
}
impl Display for ThemeVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ThemeVariant::Dark => write!(f, "dark"),
ThemeVariant::Light => write!(f, "light"),
}
}
}
impl From<ThemeVariant> for flavours::operations::generate::Mode {
fn from(value: ThemeVariant) -> Self {
match value {
ThemeVariant::Dark => Self::Dark,
ThemeVariant::Light => Self::Light,
}
}
}
pub fn generate_base16_palette(
image_path: &Path,
variant: ThemeVariant,
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
Base16ColourPalette::try_from(
&flavours::operations::generate::generate(image_path, variant.into(), false)
.unwrap_or_default(),
)
}
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
type Error = hex_color::ParseHexColorError;
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
if fixed.len() != 16 {
return Err(hex_color::ParseHexColorError::Empty);
}
Ok(Self {
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
})
}
}

View File

@@ -1,30 +1,196 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub mod colour;
mod generator;
pub use generator::ThemeVariant;
pub use generator::generate_base16_palette;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::IntoEnumIterator;
use crate::colour::Colour;
pub use base16_egui_themes::Base16;
pub use catppuccin_egui;
pub use eframe::egui::Color32;
use eframe::egui::Shadow;
use eframe::egui::Stroke;
use eframe::egui::Style;
use eframe::egui::Visuals;
use eframe::egui::style::Selection;
use eframe::egui::style::WidgetVisuals;
use eframe::egui::style::Widgets;
use serde_variant::to_variant_name;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "type")]
/// Theme
pub enum Theme {
/// A theme from catppuccin-egui
/// Theme from catppuccin-egui
Catppuccin {
name: Catppuccin,
accent: Option<CatppuccinValue>,
},
/// A theme from base16-egui-themes
/// Theme from base16-egui-themes
Base16 {
name: Base16,
accent: Option<Base16Value>,
},
/// Custom base16 palette
Custom {
palette: Box<Base16ColourPalette>,
accent: Option<Base16Value>,
},
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
/// Base16 colour palette: https://github.com/chriskempson/base16
pub struct Base16ColourPalette {
/// Base00
pub base_00: Colour,
/// Base01
pub base_01: Colour,
/// Base02
pub base_02: Colour,
/// Base03
pub base_03: Colour,
/// Base04
pub base_04: Colour,
/// Base05
pub base_05: Colour,
/// Base06
pub base_06: Colour,
/// Base07
pub base_07: Colour,
/// Base08
pub base_08: Colour,
/// Base09
pub base_09: Colour,
/// Base0A
pub base_0a: Colour,
/// Base0B
pub base_0b: Colour,
/// Base0C
pub base_0c: Colour,
/// Base0D
pub base_0d: Colour,
/// Base0E
pub base_0e: Colour,
/// Base0F
pub base_0f: Colour,
}
impl Base16ColourPalette {
pub fn background(self) -> Color32 {
self.base_01.into()
}
pub fn style(self) -> Style {
let original = Style::default();
Style {
visuals: Visuals {
widgets: Widgets {
noninteractive: WidgetVisuals {
bg_fill: self.base_01.into(),
weak_bg_fill: self.base_01.into(),
bg_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.widgets.noninteractive.bg_stroke
},
fg_stroke: Stroke {
color: self.base_05.into(),
..original.visuals.widgets.noninteractive.fg_stroke
},
..original.visuals.widgets.noninteractive
},
inactive: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
..original.visuals.widgets.inactive.bg_stroke
},
fg_stroke: Stroke {
color: self.base_05.into(),
..original.visuals.widgets.inactive.fg_stroke
},
..original.visuals.widgets.inactive
},
hovered: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: self.base_03.into(),
..original.visuals.widgets.hovered.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.hovered.fg_stroke
},
..original.visuals.widgets.hovered
},
active: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: self.base_03.into(),
..original.visuals.widgets.hovered.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.hovered.fg_stroke
},
..original.visuals.widgets.active
},
open: WidgetVisuals {
bg_fill: self.base_01.into(),
weak_bg_fill: self.base_01.into(),
bg_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.widgets.open.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.open.fg_stroke
},
..original.visuals.widgets.open
},
},
selection: Selection {
bg_fill: self.base_02.into(),
stroke: Stroke {
color: self.base_06.into(),
..original.visuals.selection.stroke
},
},
hyperlink_color: self.base_08.into(),
faint_bg_color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
extreme_bg_color: self.base_00.into(),
code_bg_color: self.base_02.into(),
warn_fg_color: self.base_0c.into(),
error_fg_color: self.base_0b.into(),
window_shadow: Shadow {
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
..original.visuals.window_shadow
},
window_fill: self.base_01.into(),
window_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.window_stroke
},
panel_fill: self.base_01.into(),
popup_shadow: Shadow {
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
..original.visuals.popup_shadow
},
..original.visuals
},
..original
}
}
}
impl Theme {
@@ -45,59 +211,110 @@ impl Theme {
.to_string()
})
.collect(),
Theme::Custom { .. } => vec!["Custom".to_string()],
}
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Base16 value
pub enum Base16Value {
/// Base00
Base00,
/// Base01
Base01,
/// Base02
Base02,
/// Base03
Base03,
/// Base04
Base04,
/// Base05
Base05,
/// Base06
#[default]
Base06,
/// Base07
Base07,
/// Base08
Base08,
/// Base09
Base09,
/// Base0A
Base0A,
/// Base0B
Base0B,
/// Base0C
Base0C,
/// Base0D
Base0D,
/// Base0E
Base0E,
/// Base0F
Base0F,
}
/// Wrapper around a Base16 colour palette
pub enum Base16Wrapper {
/// Predefined Base16 colour palette
Base16(Base16),
/// Custom Base16 colour palette
Custom(Box<Base16ColourPalette>),
}
impl Base16Value {
pub fn color32(&self, theme: Base16) -> Color32 {
match self {
Base16Value::Base00 => theme.base00(),
Base16Value::Base01 => theme.base01(),
Base16Value::Base02 => theme.base02(),
Base16Value::Base03 => theme.base03(),
Base16Value::Base04 => theme.base04(),
Base16Value::Base05 => theme.base05(),
Base16Value::Base06 => theme.base06(),
Base16Value::Base07 => theme.base07(),
Base16Value::Base08 => theme.base08(),
Base16Value::Base09 => theme.base09(),
Base16Value::Base0A => theme.base0a(),
Base16Value::Base0B => theme.base0b(),
Base16Value::Base0C => theme.base0c(),
Base16Value::Base0D => theme.base0d(),
Base16Value::Base0E => theme.base0e(),
Base16Value::Base0F => theme.base0f(),
pub fn color32(&self, theme: Base16Wrapper) -> Color32 {
match theme {
Base16Wrapper::Base16(theme) => match self {
Base16Value::Base00 => theme.base00(),
Base16Value::Base01 => theme.base01(),
Base16Value::Base02 => theme.base02(),
Base16Value::Base03 => theme.base03(),
Base16Value::Base04 => theme.base04(),
Base16Value::Base05 => theme.base05(),
Base16Value::Base06 => theme.base06(),
Base16Value::Base07 => theme.base07(),
Base16Value::Base08 => theme.base08(),
Base16Value::Base09 => theme.base09(),
Base16Value::Base0A => theme.base0a(),
Base16Value::Base0B => theme.base0b(),
Base16Value::Base0C => theme.base0c(),
Base16Value::Base0D => theme.base0d(),
Base16Value::Base0E => theme.base0e(),
Base16Value::Base0F => theme.base0f(),
},
Base16Wrapper::Custom(colours) => match self {
Base16Value::Base00 => colours.base_00.into(),
Base16Value::Base01 => colours.base_01.into(),
Base16Value::Base02 => colours.base_02.into(),
Base16Value::Base03 => colours.base_03.into(),
Base16Value::Base04 => colours.base_04.into(),
Base16Value::Base05 => colours.base_05.into(),
Base16Value::Base06 => colours.base_06.into(),
Base16Value::Base07 => colours.base_07.into(),
Base16Value::Base08 => colours.base_08.into(),
Base16Value::Base09 => colours.base_09.into(),
Base16Value::Base0A => colours.base_0a.into(),
Base16Value::Base0B => colours.base_0b.into(),
Base16Value::Base0C => colours.base_0c.into(),
Base16Value::Base0D => colours.base_0d.into(),
Base16Value::Base0E => colours.base_0e.into(),
Base16Value::Base0F => colours.base_0f.into(),
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Catppuccin palette
pub enum Catppuccin {
/// Frappe (https://catppuccin.com/palette#flavor-frappe)
Frappe,
/// Latte (https://catppuccin.com/palette#flavor-latte)
Latte,
/// Macchiato (https://catppuccin.com/palette#flavor-macchiato)
Macchiato,
/// Mocha (https://catppuccin.com/palette#flavor-mocha)
Mocha,
}
@@ -119,33 +336,60 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Catppuccin Value
pub enum CatppuccinValue {
/// Rosewater
Rosewater,
/// Flamingo
Flamingo,
/// Pink
Pink,
/// Mauve
Mauve,
/// Red
Red,
/// Maroon
Maroon,
/// Peach
Peach,
/// Yellow
Yellow,
/// Green
Green,
/// Teal
Teal,
/// Sky
Sky,
/// Sapphire
Sapphire,
/// Blue
Blue,
/// Lavender
Lavender,
#[default]
/// Text
Text,
/// Subtext1
Subtext1,
/// Subtext0
Subtext0,
/// Overlay2
Overlay2,
/// Overlay1
Overlay1,
/// Overlay0
Overlay0,
/// Surface2
Surface2,
/// Surface1
Surface1,
/// Surface0
Surface0,
/// Base
Base,
/// Mantle
Mantle,
/// Crust
Crust,
}
@@ -185,3 +429,275 @@ impl CatppuccinValue {
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
/// Theme from catppuccin-egui
pub struct KomorebiThemeCatppuccin {
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
pub name: Catppuccin,
/// Single window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
pub single_border: Option<CatppuccinValue>,
/// Stack window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
pub stack_border: Option<CatppuccinValue>,
/// Monocle window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Pink)))]
pub monocle_border: Option<CatppuccinValue>,
/// Floating window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Yellow)))]
pub floating_border: Option<CatppuccinValue>,
/// Unfocused window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
pub unfocused_border: Option<CatppuccinValue>,
/// Unfocused locked window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Red)))]
pub unfocused_locked_border: Option<CatppuccinValue>,
#[cfg(target_os = "windows")]
/// Stackbar focused text colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
pub stackbar_focused_text: Option<CatppuccinValue>,
#[cfg(target_os = "windows")]
/// Stackbar unfocused text colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Text)))]
pub stackbar_unfocused_text: Option<CatppuccinValue>,
#[cfg(target_os = "windows")]
/// Stackbar background colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
pub stackbar_background: Option<CatppuccinValue>,
/// Bar accent colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
pub bar_accent: Option<CatppuccinValue>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
/// Theme from base16-egui-themes
pub struct KomorebiThemeBase16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
pub name: Base16,
/// Single window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
pub single_border: Option<Base16Value>,
/// Stack window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
pub stack_border: Option<Base16Value>,
/// Monocle window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
pub monocle_border: Option<Base16Value>,
/// Floating window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
pub floating_border: Option<Base16Value>,
/// Unfocused window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
pub unfocused_border: Option<Base16Value>,
/// Unfocused locked window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
pub unfocused_locked_border: Option<Base16Value>,
#[cfg(target_os = "windows")]
/// Stackbar focused text colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
pub stackbar_focused_text: Option<Base16Value>,
#[cfg(target_os = "windows")]
/// Stackbar unfocused text colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
pub stackbar_unfocused_text: Option<Base16Value>,
#[cfg(target_os = "windows")]
/// Stackbar background colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
pub stackbar_background: Option<Base16Value>,
/// Bar accent colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
pub bar_accent: Option<Base16Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
/// Custom Base16 theme
pub struct KomorebiThemeCustom {
/// Colours of the custom Base16 theme palette
pub colours: Box<Base16ColourPalette>,
/// Single window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
pub single_border: Option<Base16Value>,
/// Stack window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
pub stack_border: Option<Base16Value>,
/// Monocle window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
pub monocle_border: Option<Base16Value>,
/// Floating window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
pub floating_border: Option<Base16Value>,
/// Unfocused window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
pub unfocused_border: Option<Base16Value>,
/// Unfocused locked window border colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
pub unfocused_locked_border: Option<Base16Value>,
#[cfg(target_os = "windows")]
/// Stackbar focused text colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
pub stackbar_focused_text: Option<Base16Value>,
#[cfg(target_os = "windows")]
/// Stackbar unfocused text colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
pub stackbar_unfocused_text: Option<Base16Value>,
#[cfg(target_os = "windows")]
/// Stackbar background colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
pub stackbar_background: Option<Base16Value>,
/// Bar accent colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
pub bar_accent: Option<Base16Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(tag = "palette")]
/// Komorebi theme
pub enum KomorebiTheme {
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
/// Theme from catppuccin-egui
Catppuccin(KomorebiThemeCatppuccin),
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
/// Theme from base16-egui-themes
Base16(KomorebiThemeBase16),
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
/// Custom Base16 theme
Custom(KomorebiThemeCustom),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
/// Theme from catppuccin-egui
pub struct KomobarThemeCatppuccin {
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
pub name: Catppuccin,
/// Accent colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
pub accent: Option<CatppuccinValue>,
/// Auto select fill colour
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_select_fill: Option<CatppuccinValue>,
/// Auto select text colour
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_select_text: Option<CatppuccinValue>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
/// Theme from base16-egui-themes
pub struct KomobarThemeBase16 {
/// Name of the Base16 theme (previews: https://tinted-theming.github.io/tinted-gallery/)
pub name: Base16,
/// Accent colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
pub accent: Option<Base16Value>,
/// Auto select fill colour
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_select_fill: Option<Base16Value>,
/// Auto select text colour
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_select_text: Option<Base16Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
/// Theme from base16-egui-themes
pub struct KomobarThemeCustom {
/// Colours of the custom Base16 theme palette
pub colours: Box<Base16ColourPalette>,
/// Accent colour
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
pub accent: Option<Base16Value>,
/// Auto select fill colour
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_select_fill: Option<Base16Value>,
/// Auto select text colour
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_select_text: Option<Base16Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(tag = "palette")]
/// Komorebi bar theme
pub enum KomobarTheme {
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
/// Theme from catppuccin-egui
Catppuccin(KomobarThemeCatppuccin),
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
/// Theme from base16-egui-themes
Base16(KomobarThemeBase16),
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
/// Custom Base16 theme
Custom(KomobarThemeCustom),
}
impl From<KomorebiTheme> for KomobarTheme {
fn from(value: KomorebiTheme) -> Self {
match value {
KomorebiTheme::Catppuccin(KomorebiThemeCatppuccin {
name, bar_accent, ..
}) => Self::Catppuccin(KomobarThemeCatppuccin {
name,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
}),
KomorebiTheme::Base16(KomorebiThemeBase16 {
name, bar_accent, ..
}) => Self::Base16(KomobarThemeBase16 {
name,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
}),
KomorebiTheme::Custom(KomorebiThemeCustom {
colours,
bar_accent,
..
}) => Self::Custom(KomobarThemeCustom {
colours,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
}),
}
}
}

View File

@@ -1,37 +1,39 @@
[package]
name = "komorebi"
version = "0.1.35"
version = "0.1.40"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-themes = { path = "../komorebi-themes" }
base64 = "0.22"
bitflags = { version = "2", features = ["serde"] }
clap = { workspace = true }
chrono = { workspace = true }
color-eyre = { workspace = true }
crossbeam-channel = { workspace = true }
crossbeam-utils = { workspace = true }
ctrlc = { version = "3", features = ["termination"] }
dirs = { workspace = true }
dunce = { workspace = true }
getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
ed25519-dalek = "2"
hotwatch = { workspace = true }
lazy_static = { workspace = true }
miow = "0.6"
nanoid = "0.4"
net2 = "0.2"
os_info = "3.10"
parking_lot = "0.12"
parking_lot = { workspace = true }
paste = { workspace = true }
powershell_script = "1.0"
regex = "1"
schemars = { workspace = true }
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_json = { workspace = true, features = ["preserve_order"] }
serde_yaml = { workspace = true }
shadow-rs = { workspace = true }
strum = { workspace = true }
@@ -49,12 +51,16 @@ windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.55"
serde_with = { version = "3.12", features = ["schemars_1"] }
[build-dependencies]
shadow-rs = { workspace = true }
[dev-dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
uuid = { version = "1", features = ["v4"] }
[features]
default = ["schemars"]
deadlock_detection = ["parking_lot/deadlock_detection"]
schemars = ["dep:schemars"]

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