Compare commits

...

26 Commits

Author SHA1 Message Date
dependabot[bot]
1e8c1355ad chore(deps): bump actions/github-script from 8 to 9
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 15:59:35 +00:00
LGUG2Z
26b1464381 fix(borders): prevent use-after-free take 4
This commit fixes a cross-thread use-after-free crash with exception
code 0xc000041d (FATAL_USER_CALLBACK_EXCEPTION), identified via WinDbg
analysis of a minidump where rax=0xfeeefeeefeeefeee at the crash site in
d2d1!HwndPresenter::Present - the Windows heap freed-memory fill pattern
confirming Direct2D dereferenced a previously freed object.

The root cause was a data race between the border manager thread and the
border's own message loop thread. Border::create() spawns a dedicated
thread (Thread B) for the HWND message loop and sends Box<Border> back
to the border manager thread (Thread A) via a channel. After this point
both threads accessed Border::render_target concurrently without any
synchronisation:

Thread A called update_brushes() directly on the Box<Border>, replacing
render_target with a new ID2D1HwndRenderTarget and dropping the old one.
Dropping the old RenderTarget decremented the COM refcount to zero,
causing D2D to free its internal HwndPresenter.

Thread B was concurrently mid-render in a WM_PAINT or
EVENT_OBJECT_LOCATIONCHANGE handler, holding a reference to that same
old render target obtained via the GWLP_USERDATA raw pointer. Calling
EndDraw() after HwndPresenter was freed produced the crash. The process
uptime of two seconds in the dump confirmed this happened during startup
workspace initialisation, when a ForceUpdate notification triggered
update_brushes() while the newly-shown border window was processing its
first WM_PAINT.

The fix routes all brush update requests through the border's own
message loop by posting a custom WM_UPDATE_BRUSHES (WM_USER + 1) message
instead of calling update_brushes() cross-thread. The three call sites
in the border manager that previously called border.update_brushes()?
directly now call border.request_brush_update(), which posts the message
via PostMessageW. The WndProc handler for WM_UPDATE_BRUSHES calls
update_brushes() and invalidate() entirely on Thread B, eliminating the
race.

A secondary bug in destroy() was also fixed: it was clearing
GWLP_USERDATA before posting WM_CLOSE, which caused WM_DESTROY's null-
pointer guard to skip the render_target = None cleanup. This left the
ID2D1HwndRenderTarget alive past HWND destruction, and D2D freed its
HwndPresenter during WM_NCDESTROY while the COM wrapper still held a
reference - a second path to the same crash for any message queued
between WM_NCDESTROY and WM_QUIT. The premature GWLP_USERDATA clear has
been removed; WM_DESTROY already handles it correctly after releasing
the render target.
2026-03-29 19:09:13 -07:00
pro470
d7580d2271 feat(wm): add apply state socket message
This allows clients to send a state and get it applied to the wm.

By using the already there apply_state function we have all the
safety we need to apply the sent state.

This is also intentionally not open for the komorebic binary because I
think the average user doesnt need such a command and this is much safer
also to not give them the ability.
2026-03-22 20:06:43 -07:00
LGUG2Z
8a1447f543 docs(schema): update jsonschema 2026-03-22 16:11:43 -07:00
LGUG2Z
53c81c4596 fix(wm): factor in monocle mode for work area offset rules 2026-03-22 16:11:43 -07:00
omark96
d3779e5a74 feat(wm): add dynamic work area offsets
This commit adds a config setting to let you set custom
work_area_offset_rules for a workspace, following the same schema as
layout_rules.
2026-03-22 16:11:43 -07:00
1337cookie
c84fa50fc9 fear(bar): add storage_display_name option 2026-03-22 16:11:43 -07:00
bunnyDrug
41fc316a59 fix(docs): add missing word
Reading the documentation I came across what I think is a missing word
in this sentence.
2026-03-22 16:11:43 -07:00
LGUG2Z
011bcb8bd4 refactor(clippy): apply lints 2026-03-22 16:11:43 -07:00
LGUG2Z
dce3c91c22 chore(deps): various dependency bumps 2026-03-22 16:11:43 -07:00
LGUG2Z
a9a1e68169 chore(git): add procdump.exe to gitignore 2026-03-21 13:39:50 -07:00
dependabot[bot]
cb9a7542a6 chore(deps): bump actions/download-artifact from 7 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-21 13:10:32 -07:00
dependabot[bot]
145a0ae003 chore(deps): bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-21 13:10:22 -07:00
Melvin Tan
96e87d8ae0 fix(docs): update broken configuration schema link 2026-03-21 13:09:43 -07:00
LGUG2Z
529d93595e chore(deps): cargo update 2026-03-21 12:38:45 -07:00
Csaba
ea35d818a1 feat(bar): windows systray widget
A new System Tray widget has been added to komorebi-bar, bringing native
Windows system tray functionality directly into the bar.

Special thanks to the Zebar project and its contributors for developing
the systray-util crate library that makes Windows system tray
integration possible.

The widget intercepts system tray icon data by creating a hidden "spy"
window that mimics the Windows taskbar. When applications use the
Shell_NotifyIcon API to manage their tray icons, the widget receives the
same broadcast messages, allowing it to monitor all system tray activity
while forwarding messages to the real taskbar to avoid disruption.

Users can configure which icons to hide using flexible rules. A plain
string matches by exe name (case-insensitive). A structured object can
match on exe, tooltip, and/or GUID fields using AND logic. Each field
supports matching strategies from komorebi's window rules (Equals,
StartsWith, EndsWith, Contains, Regex, and their negated variants),
allowing precise filtering even when multiple icons share the same exe
and GUID. An info button opens a floating panel listing all icons with
their properties and copy buttons, making it easy to identify which
values to use in filter rules.

The widget fully supports mouse interactions including left-click,
right-click, middle-click, and double-click actions on tray icons.
Double-click support uses the LeftDoubleClick action from systray-util
0.2.0, which sends WM_LBUTTONDBLCLK and NIN_SELECT messages. It handles
right-aligned placement correctly by adjusting the rendering order and
toggle button arrow directions to maintain consistent visual appearance
regardless of which panel the widget is placed in.

Some system tray icons register a click callback but never actually
respond to click messages, effectively becoming "zombie" icons from an
interaction standpoint. The widget includes fallback commands for known
problematic icons that override the native click action with a direct
shell command (e.g. opening Windows Security or volume settings).

The implementation uses a background thread with its own tokio runtime
to handle the async systray events, communicating with the UI thread
through crossbeam channels. Icon images are cached efficiently using
hash-based keys that update when icons change. Rapid icon updates are
deduplicated to prevent UI freezing, and image conversion (RgbaImage to
ColorImage) is performed on the background thread to keep the UI
responsive.

The widget automatically detects and removes stale icons whose owning
process has exited, using the Win32 IsWindow API on a configurable
interval. A manual refresh button is also available for immediate
cleanup.

A shortcuts button can be configured to toggle komorebi-shortcuts by
killing the process if running or starting it otherwise. The refresh,
info, and shortcuts buttons can each be placed in the main visible area
or in the overflow section.
2026-03-21 12:35:39 -07:00
LGUG2Z
5f629e1f1a feat(wm): cycle monocle container when focusing in direction
This commit implements something I've found myself wanting while working
on the Macbook.

When I have a monocle container active, I want to quickly be able to
switch back and forth with an adjacent window in the underlying layout
without having to toggle monocle mode off and back on again.

This is now accomplished by using focus left/down to promote the
previous window in the Ring to monocle, and by using focus right/down to
focus the next window in the Ring to monocle.

Borders were being funny so I just ended up nuking them whenever we
cycle.
2026-03-08 19:49:50 -07:00
LGUG2Z
0f1854db8b ci(github): re-enable sunday cron jobs 2026-03-01 21:33:51 -08:00
Csaba
8889c3ca93 fix(wm): correct layout rounding for uneven integer division
The grid layout calculates row heights using integer division, which
truncates the result when the area height is not evenly divisible by the
number of rows in a column. Since every row received this truncated
height, columns with an odd row count (e.g. 3 rows in 800px: 266*3=798)
would fall short of the full area height, leaving a visible gap at the
bottom compared to columns with an even row count (e.g. 2 rows:
400*2=800).

The last row in each column now absorbs the remainder pixels from the
integer division. The vertical flip position calculation was also
updated so that the last row, which becomes the topmost window when
flipped, starts at the area top edge instead of being offset by the
truncation error.

The same integer division truncation also affected column widths in the
grid layout when the area width is not evenly divisible by the number of
columns. The last column now absorbs the width remainder using the same
pattern, and this correction is applied before the flipped column
positions are calculated so that horizontal flips also tile correctly.

The two shared helper functions columns_with_ratios and rows_with_ratios
had the same issue, which propagated to every layout that delegates to
them: Columns, Rows, VerticalStack stack rows, RightMainVerticalStack
stack rows, HorizontalStack stack columns, and UltrawideVerticalStack
tertiary rows. Both functions now correct the last element after the
sizing loop so that the total tiled dimension matches the area exactly.

The Scrolling layout computes a uniform column width by dividing the
area width by the visible column count, which can also leave a remainder
gap on the right edge. The last visible column in the current viewport
now absorbs this remainder. Since the layout is recalculated on every
scroll event, the correction stays accurate regardless of which column
is rightmost.
2026-03-01 21:31:36 -08:00
LGUG2Z
6ca49d4301 chore(deps): cargo update 2026-02-20 16:58:40 -08:00
Csaba
634a3e7f3b fix(wm): correct bsp/grid container positioning when flipped
The recursive_fibonacci function used formulas for main_x and main_y
that mixed unresized area dimensions with resized dimensions when
calculating flipped positions.

This caused a gap between containers proportional to resize_delta * (2 *
ratio - 1), meaning any column_ratios or row_ratios value other than the
default 0.5 would produce visible gaps or overlaps between containers
when the layout was both flipped and resized.

The fix replaces the flipped position formulas with ones that derive
main_x and main_y directly from the alt area width and height
expressions used by the recursive child call, ensuring the main
container is always positioned immediately adjacent to the alt area
regardless of ratio or resize delta.

The horizontal flip for grid layouts swapped column left positions
directly from the unflipped layout. With non-default column ratios this
caused narrow columns to receive wide column positions, producing
overlapping containers.

Precompute flipped column left positions by laying out the original
column widths in reverse order from the area left edge, ensuring
containers tile without overlap regardless of column ratio.

Separated all unit test for arrangements into a new file.

Added 4 BSP-specific regression tests (horizontal flip, vertical flip,
both axes, sweep across multiple ratios/deltas).

Added 7 adjacency tests covering ALL other layouts (Columns, Rows,
VerticalStack, RightMainVerticalStack, HorizontalStack,
UltrawideVerticalStack, Scrolling) confirming they don't have the gap
issue.

Added 2 Grid tests that verify containers don't overlap and tile the
full area when column ratios are non-default. The first test checks a
horizontal flip specifically, confirming 2 columns form with no gaps
edge-to-edge. The second test runs all three flip axes and asserts no
overlaps and full area coverage for each.
2026-02-20 16:57:21 -08:00
LGUG2Z
5b6fab0044 chore(dev): begin v0.1.41-dev 2026-02-20 16:56:51 -08:00
LGUG2Z
bed314b866 chore(release): v0.1.40 2026-02-14 12:52:25 -08:00
Csaba
c165172b5a feat(cli): new layout-ratios command
The implementation adds a new layout-ratio CLI command to komorebi that
allows users to dynamically set column and row ratios for layouts at
runtime via komorebic layout-ratio --columns 0.3 0.4 --rows 0.5.

A public validate_ratios function was added to komorebi-layouts that
clamps ratio values between 0.1 and 0.9, truncates when the cumulative
sum would reach or exceed 1.0, and limits to a maximum of 5 ratios. This
function is shared between config file deserialization and the new CLI
command, ensuring consistent validation behavior.

The SocketMessage enum in komorebi/src/core/mod.rs has a new
LayoutRatios variant. The handler in process_command.rs uses the shared
validate_ratios function to process the ratios before applying them to
the focused workspace's layout options.

When the CLI command is called without any arguments, it prints a
helpful message instead of returning an error.
2026-02-13 11:24:57 -08:00
Csaba
9977cca500 feat(bar): enhancing the media widget
The Media widget has been enhanced with a new MediaDisplayFormat
configuration option that controls how the widget is displayed. It
supports seven formats: Icon, Text, IconAndText (the default),
ControlsOnly, IconAndControls, TextAndControls, and Full.

The widget now detects whether the Previous and Next buttons are
actually available for the current media session using the Windows Media
Control API. When a button is not available, it appears dimmed at 50
percent opacity and clicking it has no effect.

Tooltips were added to improve usability. Hovering over the media info
label shows the full artist and title text, which is helpful when the
text is truncated. The Play/Pause button also shows the media info on
hover.

The rendering logic was refactored to properly handle right-aligned
widgets.

When the Media widget is placed in right_widgets, the UI renders items
from right to left, so the code now renders elements in reverse order to
ensure the visual appearance remains consistent regardless of which
panel the widget is placed in.
2026-02-12 20:39:16 -08:00
LGUG2Z
5d7a0ea9ad chore(deps): cargo update 2026-02-12 20:35:29 -08:00
46 changed files with 6750 additions and 1439 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.json text diff

View File

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

View File

@@ -13,8 +13,8 @@ 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:
@@ -65,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@v6
- uses: actions/upload-artifact@v7
with:
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: |
@@ -87,7 +87,7 @@ jobs:
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
- 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
@@ -136,7 +136,7 @@ jobs:
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
- 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
@@ -178,7 +178,7 @@ jobs:
run: |
TAG=${{ github.ref_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
- 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

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ komorebic/applications.json
/.xwin-cache
result
/.direnv
procdump.exe

1260
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,13 +30,13 @@ lazy_static = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
serde_yaml = "0.9"
strum = { version = "0.27", features = ["derive"] }
strum = { version = "0.28", 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.37"
sysinfo = "0.38"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
windows-numerics = { version = "0.3" }

View File

@@ -79,7 +79,7 @@ Please refer to the [documentation](https://lgug2z.github.io/komorebi) for instr
to [install](https://lgug2z.github.io/komorebi/installation.html) and
[configure](https://lgug2z.github.io/komorebi/example-configurations.html)
_komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflows/komorebi-config-home.html), a complete
[configuration schema reference](https://komorebi.lgug2z.com/schema) and a
[configuration schema reference](https://komorebi-starlight.lgug2z.workers.dev/reference/komorebi-windows/) and a
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
## Community
@@ -424,7 +424,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.39"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi" }
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -22,12 +22,12 @@
"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",
"anstream 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"anstyle 1.0.14 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-parse 1.0.0 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",
"anyhow 1.0.102 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",
@@ -41,23 +41,23 @@
"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",
"bitflags 2.11.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.25.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.55 registry+https://github.com/rust-lang/crates.io-index",
"cc 1.2.57 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.43 registry+https://github.com/rust-lang/crates.io-index",
"chrono 0.4.44 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.57 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.5.57 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.5.55 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 0.7.7 registry+https://github.com/rust-lang/crates.io-index",
"clap 4.6.0 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.6.0 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.6.0 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 1.1.0 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",
"colorchoice 1.0.5 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",
@@ -66,11 +66,11 @@
"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",
"ctrlc 3.5.2 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",
"deranged 0.5.8 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",
@@ -95,7 +95,6 @@
"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",
@@ -107,18 +106,19 @@
"flate2 1.1.9 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",
"futures 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-channel 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-core 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-executor 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-io 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-macro 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-sink 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-task 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-util 0.3.32 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.17 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.4.2 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.4 registry+https://github.com/rust-lang/crates.io-index",
@@ -144,25 +144,25 @@
"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 0.25.10 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",
"ipnet 2.12.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",
"itoa 1.0.18 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",
"libc 0.2.183 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",
"libz-sys 1.1.25 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",
@@ -177,11 +177,11 @@
"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",
"moxcms 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
"native-tls 0.2.18 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",
"ntapi 0.4.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",
@@ -193,7 +193,7 @@
"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.37.3 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 1.21.4 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",
@@ -203,21 +203,21 @@
"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-project-lite 0.2.17 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",
"png 0.18.1 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.106 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.29 registry+https://github.com/rust-lang/crates.io-index",
"pxfm 0.1.27 registry+https://github.com/rust-lang/crates.io-index",
"psm 0.1.30 registry+https://github.com/rust-lang/crates.io-index",
"pxfm 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.44 registry+https://github.com/rust-lang/crates.io-index",
"quote 1.0.45 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",
@@ -235,13 +235,13 @@
"ref-cast-impl 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
"regex 1.12.3 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.4.14 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.10 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.27 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.14.0 registry+https://github.com/rust-lang/crates.io-index",
"ryu 1.0.22 registry+https://github.com/rust-lang/crates.io-index",
"ryu 1.0.23 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",
@@ -252,12 +252,12 @@
"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_with 3.18.0 registry+https://github.com/rust-lang/crates.io-index",
"serde_with_macros 3.18.0 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.7.0 registry+https://github.com/rust-lang/crates.io-index",
"shadow-rs 1.7.1 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",
@@ -266,31 +266,31 @@
"siphasher 1.0.2 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.2 registry+https://github.com/rust-lang/crates.io-index",
"socket2 0.6.3 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",
"stacker 0.1.23 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",
"syn 2.0.117 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",
"tempfile 3.27.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.18 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 2.0.18 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.46 registry+https://github.com/rust-lang/crates.io-index",
"time 0.3.47 registry+https://github.com/rust-lang/crates.io-index",
"time-core 0.1.8 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.3 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",
"tzdb_data 0.2.4 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-ident 1.0.24 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",
@@ -300,10 +300,11 @@
"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",
"uuid 1.22.0 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",
"webbrowser 1.2.0 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.2 registry+https://github.com/rust-lang/crates.io-index",
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
@@ -363,17 +364,15 @@
"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",
"winit 0.30.13 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.38 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.38 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.47 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.47 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.1 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.12 registry+https://github.com/rust-lang/crates.io-index"
"zune-jpeg 0.5.14 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
@@ -395,8 +394,8 @@
"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.38 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.38 registry+https://github.com/rust-lang/crates.io-index"
"zerocopy 0.8.47 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.47 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
@@ -404,7 +403,7 @@
[
"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",
"avif-serialize 0.8.8 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",
@@ -412,9 +411,9 @@
"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",
"moxcms 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
"pxfm 0.1.28 registry+https://github.com/rust-lang/crates.io-index",
"ravif 0.13.0 registry+https://github.com/rust-lang/crates.io-index",
"subtle 2.6.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
@@ -423,7 +422,7 @@
[
"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"
"ryu 1.0.23 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
@@ -457,12 +456,12 @@
"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",
"anstream 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"anstyle 1.0.14 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-parse 1.0.0 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",
"anyhow 1.0.102 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",
@@ -477,7 +476,7 @@
"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",
"bitflags 2.11.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",
@@ -491,22 +490,22 @@
"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.55 registry+https://github.com/rust-lang/crates.io-index",
"cc 1.2.57 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.43 registry+https://github.com/rust-lang/crates.io-index",
"chrono 0.4.44 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.57 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.5.57 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.5.55 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 0.7.7 registry+https://github.com/rust-lang/crates.io-index",
"clap 4.6.0 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.6.0 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.6.0 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 1.1.0 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",
"colorchoice 1.0.5 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",
@@ -515,14 +514,14 @@
"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",
"ctrlc 3.5.2 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",
"darling 0.23.0 registry+https://github.com/rust-lang/crates.io-index",
"darling_core 0.23.0 registry+https://github.com/rust-lang/crates.io-index",
"darling_macro 0.23.0 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",
"deranged 0.5.8 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",
@@ -546,7 +545,6 @@
"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",
@@ -565,19 +563,20 @@
"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",
"futures 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-channel 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-core 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-executor 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-io 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-macro 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-sink 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-task 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"futures-util 0.3.32 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.17 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.4.2 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.4 registry+https://github.com/rust-lang/crates.io-index",
@@ -606,23 +605,23 @@
"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 0.25.10 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",
"ipnet 2.12.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",
"itoa 1.0.18 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",
"libc 0.2.183 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",
"libz-sys 1.1.25 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",
@@ -634,7 +633,7 @@
"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",
"memchr 2.8.0 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",
@@ -645,15 +644,15 @@
"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",
"native-tls 0.2.18 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",
"netdev 0.40.1 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",
"ntapi 0.4.3 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",
@@ -666,11 +665,11 @@
"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.37.3 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 1.21.4 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",
"owo-colors 4.3.0 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",
@@ -686,21 +685,21 @@
"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-project-lite 0.2.17 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",
"png 0.18.1 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.106 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.29 registry+https://github.com/rust-lang/crates.io-index",
"psm 0.1.30 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.44 registry+https://github.com/rust-lang/crates.io-index",
"quote 1.0.45 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",
@@ -719,15 +718,15 @@
"ref-cast-impl 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
"regex 1.12.3 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.4.14 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.10 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",
"rgb 0.8.53 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.27 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.14.0 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",
"schannel 0.1.29 registry+https://github.com/rust-lang/crates.io-index",
"schemars 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"schemars_derive 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"scoped_threadpool 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
@@ -741,12 +740,12 @@
"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_with 3.18.0 registry+https://github.com/rust-lang/crates.io-index",
"serde_with_macros 3.18.0 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.7.0 registry+https://github.com/rust-lang/crates.io-index",
"shadow-rs 1.7.1 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",
@@ -759,29 +758,31 @@
"slab 0.4.12 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.2 registry+https://github.com/rust-lang/crates.io-index",
"socket2 0.6.3 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",
"stacker 0.1.23 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",
"syn 2.0.117 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",
"sysinfo 0.38.4 registry+https://github.com/rust-lang/crates.io-index",
"systray-util 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"tempfile 3.27.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.18 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 2.0.18 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.11.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.46 registry+https://github.com/rust-lang/crates.io-index",
"time 0.3.47 registry+https://github.com/rust-lang/crates.io-index",
"time-core 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
"tokio 1.49.0 registry+https://github.com/rust-lang/crates.io-index",
"tokio 1.50.0 registry+https://github.com/rust-lang/crates.io-index",
"tokio-macros 2.6.1 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",
@@ -795,15 +796,15 @@
"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",
"tracing-subscriber 0.3.23 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.3 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",
"tzdb_data 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"uds_windows 1.2.1 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-ident 1.0.24 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",
@@ -813,14 +814,15 @@
"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",
"uuid 1.22.0 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",
"webbrowser 1.2.0 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",
"which 8.0.2 registry+https://github.com/rust-lang/crates.io-index",
"win-msgbox 0.2.2 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",
@@ -884,27 +886,24 @@
"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.38 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.38 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.47 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy-derive 0.8.47 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.19 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"zmij 1.0.21 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.5.1 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.12 registry+https://github.com/rust-lang/crates.io-index"
"zune-jpeg 0.5.14 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"
"tzdb_data 0.2.4 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
@@ -939,7 +938,7 @@
"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",
"unicode-ident 1.0.24 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",
@@ -956,7 +955,7 @@
"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",
"memchr 2.8.0 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"
@@ -976,11 +975,9 @@
"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.1 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.12 registry+https://github.com/rust-lang/crates.io-index"
"zune-jpeg 0.5.14 registry+https://github.com/rust-lang/crates.io-index"
]
]
]

View File

@@ -0,0 +1,218 @@
# System Tray
The System Tray widget brings native Windows system tray icons into
`komorebi-bar`. It intercepts tray icon data by creating a hidden window that
mimics the Windows taskbar, receiving the same broadcast messages that
applications send via `Shell_NotifyIcon`.
## Basic configuration
```json
{
"right_widgets": [
{
"Systray": {
"enable": true
}
}
]
}
```
## Hiding icons
The `hidden_icons` config field accepts a list of rules. Each rule can be either
a plain string or a structured object.
A **plain string** matches the exe name (case-insensitive). This is the original
format, so existing configs continue to work without changes:
```json
"hidden_icons": [
"SecurityHealthSystray.exe",
"PhoneExperienceHost.exe"
]
```
A **structured object** matches one or more icon properties. All specified fields
must match (AND logic). By default matching is exact and case-insensitive.
```json
"hidden_icons": [
{ "exe": "svchost.exe", "tooltip": "Some Specific App" },
{ "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" },
{ "tooltip": "App I want hidden" }
]
```
The two forms can be mixed freely:
```json
"hidden_icons": [
"PhoneExperienceHost.exe",
{ "exe": "svchost.exe", "tooltip": "Specific Notification" },
{ "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" }
]
```
Available fields for structured rules:
| Field | Description |
|-----------|----------------------------------------------------------|
| `exe` | Executable name (e.g. `"SecurityHealthSystray.exe"`) |
| `tooltip` | Tooltip text shown on hover |
| `guid` | Icon GUID — most stable identifier across app restarts |
### Matching strategies
Each field can be a plain string (exact case-insensitive match) or an object
with `value` and `matching_strategy` for advanced matching. This uses the same
`MatchingStrategy` as komorebi's window rules.
```json
"hidden_icons": [
{
"exe": "explorer.exe",
"tooltip": { "value": "Network", "matching_strategy": "StartsWith" }
}
]
```
The above hides explorer.exe icons whose tooltip starts with "Network", while
leaving other explorer.exe icons visible.
Available strategies:
| Strategy | Description |
|---------------------|---------------------------------------------------|
| `Equals` | Exact match (default when using a plain string) |
| `StartsWith` | Value starts with the given text |
| `EndsWith` | Value ends with the given text |
| `Contains` | Value contains the given text |
| `Regex` | Value matches a regular expression |
| `DoesNotEqual` | Value does not exactly equal the given text |
| `DoesNotStartWith` | Value does not start with the given text |
| `DoesNotEndWith` | Value does not end with the given text |
| `DoesNotContain` | Value does not contain the given text |
All strategies except `Regex` are case-insensitive. For case-insensitive regex,
include `(?i)` in the pattern.
Plain strings and strategy objects can be mixed across fields:
```json
{
"exe": "explorer.exe",
"tooltip": { "value": "notification", "matching_strategy": "Contains" }
}
```
Run komorebi-bar with `RUST_LOG=info` to see the exe, tooltip, and GUID of every
systray icon in the log output.
## Stale icon cleanup
Some applications (e.g. Docker Desktop) may exit without properly removing their
tray icon. The widget detects these stale icons by checking whether the owning
window still exists via the Win32 `IsWindow` API.
### Automatic cleanup
By default, the widget checks for stale icons every 60 seconds. The interval
can be configured with `stale_icons_check_interval` (in seconds). The value is
clamped between 30 and 600. Set to 0 to disable automatic cleanup.
```json
"stale_icons_check_interval": 120
```
### Refresh button
A manual refresh button can be shown by setting `refresh_button`. Clicking it
immediately removes any stale icons.
- `"Visible"` — shows the button in the main icon area
- `"Overflow"` — shows the button in the hidden/overflow section (appears when
the overflow toggle is expanded)
```json
"refresh_button": "Overflow"
```
When set to `"Overflow"`, the overflow toggle arrow will appear even if there are
no hidden icons, so the refresh button remains accessible.
## Info button
An info button can be shown to open a floating panel that lists all systray icons
with their exe name, tooltip, GUID, and visibility status. This is useful for
identifying which icons to filter with `hidden_icons` rules.
- `"Visible"` — shows the button in the main icon area
- `"Overflow"` — shows the button in the hidden/overflow section
```json
"info_button": "Visible"
```
The info panel shows **all** icons, including those hidden by rules or the OS.
Each row shows the icon image, exe name, tooltip, GUID, and whether it is visible.
Copy buttons are provided on the exe, tooltip, and GUID cells for easy copying
(e.g. to paste a GUID into a filter rule).
Like the refresh button, setting `info_button` to `"Overflow"` will make the
overflow toggle arrow appear even if there are no hidden icons.
## Shortcuts button
A button that toggles komorebi-shortcuts. If the shortcuts process is running
it will be killed; otherwise it will be started.
- `"Visible"` — shows the button in the main icon area
- `"Overflow"` — shows the button in the hidden/overflow section
```json
"shortcuts_button": "Visible"
```
Like the other buttons, setting `shortcuts_button` to `"Overflow"` will make the
overflow toggle arrow appear even if there are no hidden icons.
## Mouse interactions
The widget supports left-click, right-click, middle-click, and double-click on
tray icons. Double-click sends the `LeftDoubleClick` action (via systray-util
0.2.0), which delivers `WM_LBUTTONDBLCLK` and `NIN_SELECT` messages to the icon.
## Click fallbacks
Some systray icons register a click callback but never actually respond to click
messages, effectively becoming "zombie" icons from an interaction standpoint. For
known problematic icons, the widget overrides the native click action with a
direct shell command. Fallback commands take priority — if a fallback is defined
for an icon, it always runs regardless of whether the icon reports itself as
clickable.
| Exe | Tooltip condition | Fallback command |
|--------------------------------|-------------------|---------------------------------|
| `SecurityHealthSystray.exe` | any | `start windowsdefender://` |
| `explorer.exe` | ends with `%` | `start ms-settings:apps-volume` |
| `explorer.exe` | empty | `start ms-settings:batterysaver`|
## Full example
```json
{
"Systray": {
"enable": true,
"hidden_icons": [
"SecurityHealthSystray.exe",
{ "exe": "explorer.exe", "tooltip": { "value": "Network", "matching_strategy": "StartsWith" } }
],
"stale_icons_check_interval": 60,
"refresh_button": "Overflow",
"info_button": "Visible",
"shortcuts_button": "Overflow"
}
}
```

View File

@@ -0,0 +1,40 @@
# Komorebi Bar
`komorebi-bar` is a status bar for komorebi that renders on top of the tiling
window manager. It is configured through a `komorebi.bar.json` file, either
alongside your `komorebi.json` or at the path specified in the
`bar_configurations` array.
## Widgets
Widgets are placed in the `left_widgets`, `center_widgets`, or `right_widgets`
arrays. Each widget is an object with the widget type as key and its
configuration as value.
| Widget | Description |
|--------------|--------------------------------------------------------|
| `Komorebi` | Workspaces, layout, focused window, and more |
| `Battery` | Battery level and charging status |
| `Date` | Current date in configurable format |
| `Time` | Current time in configurable format |
| `Media` | Currently playing media information |
| `Memory` | System memory usage |
| `Network` | Network activity and connection status |
| `Storage` | Disk usage information |
| `Update` | Komorebi update notification |
| `Systray` | Windows system tray icons |
Widgets with dedicated documentation pages:
- [System Tray](bar-widgets/systray.md)
> Dedicated pages for the remaining widgets will be added in the future.
## Schema
The full configuration schema is available at
[komorebi-bar.lgug2z.com/schema](https://komorebi-bar.lgug2z.com/schema).
For running a bar on each monitor, see
[Multiple Bar Instances](multiple-bar-instances.md) and
[Multi-Monitor Setup](multi-monitor-setup.md).

View File

@@ -83,7 +83,7 @@ is a crude hack trying to compensate for the insistence of Microsoft Windows
design teams to make custom borders with widths that are actually visible to
the user a thing of the past and removing this capability from the Win32 API.
I know it's buggy, and I know that most of the it sucks, but this is something
I know it's buggy, and I know that most of the time it sucks, but this is something
you should be bring up with the billion dollar company and not with me, the
solo developer.

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.41/schema.bar.json",
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.40"
version = "0.1.41"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -18,15 +18,17 @@ dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
egui_extras = { workspace = true }
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
lazy_static = { workspace = true }
netdev = "0.40"
netdev = "0.41"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
parking_lot = { workspace = true }
regex = "1"
random_word = { version = "0.5", features = ["en"] }
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true, optional = true }
@@ -34,6 +36,8 @@ serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
systray-util = "0.2.0"
tokio = { version = "1", features = ["rt", "sync", "time"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
which = { workspace = true }

View File

@@ -18,6 +18,7 @@ use crate::render::Color32Ext;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::take_widget_clicked;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::MonitorInfo;
use crate::widgets::widget::BarWidget;
@@ -1082,6 +1083,10 @@ impl eframe::App for Komobar {
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
CentralPanel::default().frame(frame).show(ctx, |ui| {
// Variable to store command to execute after widgets are rendered
// This allows widgets to mark clicks as consumed before bar processes them
let mut pending_command: Option<crate::config::MouseMessage> = None;
if let Some(mouse_config) = &self.config.mouse {
let command = if ui
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
@@ -1182,9 +1187,9 @@ impl eframe::App for Komobar {
&None
};
if let Some(command) = command {
command.execute(self.mouse_follows_focus);
}
// Store the command to execute after widgets are rendered
// This allows widgets to mark clicks as consumed
pending_command = command.clone();
}
// Apply grouping logic for the bar as a whole
@@ -1316,6 +1321,13 @@ impl eframe::App for Komobar {
});
});
}
// Execute the deferred mouse command only if no widget consumed the click
if let Some(command) = pending_command
&& !take_widget_clicked()
{
command.execute(self.mouse_follows_focus);
}
});
}
}

View File

@@ -15,7 +15,7 @@ use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.bar.json` configuration file reference for `v0.1.40`
/// The `komorebi.bar.json` configuration file reference for `v0.1.41`
pub struct KomobarConfig {
/// Bar height
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
@@ -621,6 +621,26 @@ extend_enum!(
AllIconsAndTextOnSelected,
});
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget display format
pub enum MediaDisplayFormat {
/// Show only the media info icon
Icon,
/// Show only the media info text (artist - title)
Text,
/// Show both icon and text
IconAndText,
/// Show only the control buttons (previous, play/pause, next)
ControlsOnly,
/// Show icon with control buttons
IconAndControls,
/// Show text with control buttons
TextAndControls,
/// Show icon, text, and control buttons
Full,
}
#[cfg(test)]
mod tests {
use serde::Deserialize;

View File

@@ -38,6 +38,8 @@ use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows_core::BOOL;
use std::sync::atomic::AtomicBool;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
@@ -46,6 +48,20 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
pub static DEFAULT_PADDING: f32 = 10.0;
/// Flag to indicate that a widget has consumed a click event this frame.
/// This prevents the bar's global mouse handler from also processing the click.
pub static WIDGET_CLICKED: AtomicBool = AtomicBool::new(false);
/// Mark that a widget has consumed a click event this frame.
pub fn mark_widget_clicked() {
WIDGET_CLICKED.store(true, Ordering::SeqCst);
}
/// Check if a widget has consumed a click event this frame and reset the flag.
pub fn take_widget_clicked() -> bool {
WIDGET_CLICKED.swap(false, Ordering::SeqCst)
}
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);

View File

@@ -1,4 +1,6 @@
use crate::MAX_LABEL_WIDTH;
use crate::bar::Alignment;
use crate::config::MediaDisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
@@ -14,6 +16,7 @@ use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -21,24 +24,31 @@ use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
/// Display format of the media widget (defaults to IconAndText)
pub display: Option<MediaDisplayFormat>,
}
impl From<MediaConfig> for Media {
fn from(value: MediaConfig) -> Self {
Self::new(value.enable)
Self::new(
value.enable,
value.display.unwrap_or(MediaDisplayFormat::IconAndText),
)
}
}
#[derive(Clone, Debug)]
pub struct Media {
pub enable: bool,
pub display: MediaDisplayFormat,
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
}
impl Media {
pub fn new(enable: bool) -> Self {
pub fn new(enable: bool, display: MediaDisplayFormat) -> Self {
Self {
enable,
display,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap()
.join()
@@ -54,6 +64,58 @@ impl Media {
}
}
pub fn previous(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(op) = session.TrySkipPreviousAsync()
{
op.join().unwrap_or_default();
}
}
pub fn next(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(op) = session.TrySkipNextAsync()
{
op.join().unwrap_or_default();
}
}
fn is_playing(&self) -> bool {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(info) = session.GetPlaybackInfo()
&& let Ok(status) = info.PlaybackStatus()
{
return status == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing;
}
false
}
fn is_previous_enabled(&self) -> bool {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(info) = session.GetPlaybackInfo()
&& let Ok(controls) = info.Controls()
&& let Ok(enabled) = controls.IsPreviousEnabled()
{
return enabled;
}
false
}
fn is_next_enabled(&self) -> bool {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(info) = session.GetPlaybackInfo()
&& let Ok(controls) = info.Controls()
&& let Ok(enabled) = controls.IsNextEnabled()
{
return enabled;
}
false
}
fn has_session(&self) -> bool {
self.session_manager.GetCurrentSession().is_ok()
}
fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
@@ -78,28 +140,96 @@ impl Media {
impl BarWidget for Media {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
// Don't render if there's no active media session
if !self.has_session() {
return;
}
let output = self.output();
if !output.is_empty() {
let mut layout_job = LayoutJob::simple(
let show_icon = matches!(
self.display,
MediaDisplayFormat::Icon
| MediaDisplayFormat::IconAndText
| MediaDisplayFormat::IconAndControls
| MediaDisplayFormat::Full
);
let show_text = matches!(
self.display,
MediaDisplayFormat::Text
| MediaDisplayFormat::IconAndText
| MediaDisplayFormat::TextAndControls
| MediaDisplayFormat::Full
);
let show_controls = matches!(
self.display,
MediaDisplayFormat::ControlsOnly
| MediaDisplayFormat::IconAndControls
| MediaDisplayFormat::TextAndControls
| MediaDisplayFormat::Full
);
// Don't render if there's no media info and we're not showing controls-only
if output.is_empty() && !show_controls {
return;
}
let icon_font_id = config.icon_font_id.clone();
let text_font_id = config.text_font_id.clone();
let icon_color = ctx.style().visuals.selection.stroke.color;
let text_color = ctx.style().visuals.text_color();
let mut layout_job = LayoutJob::default();
if show_icon {
layout_job = LayoutJob::simple(
egui_phosphor::regular::HEADPHONES.to_string(),
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
icon_font_id.clone(),
icon_color,
100.0,
);
}
if show_text {
layout_job.append(
&output,
10.0,
if show_icon { 10.0 } else { 0.0 },
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
font_id: text_font_id,
color: text_color,
valign: Align::Center,
..Default::default()
},
);
}
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
let is_playing = self.is_playing();
let is_previous_enabled = self.is_previous_enabled();
let is_next_enabled = self.is_next_enabled();
let disabled_color = text_color.gamma_multiply(0.5);
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
let prev_color = if is_previous_enabled {
text_color
} else {
disabled_color
};
let next_color = if is_next_enabled {
text_color
} else {
disabled_color
};
let play_pause_icon = if is_playing {
egui_phosphor::regular::PAUSE
} else {
egui_phosphor::regular::PLAY
};
let show_label = |ui: &mut Ui| {
if (show_icon || show_text)
&& SelectableFrame::new(false)
.show(ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
@@ -109,15 +239,95 @@ impl BarWidget for Media {
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
Label::new(layout_job.clone()).selectable(false).truncate(),
)
})
.on_hover_text(&output)
.clicked()
{
self.toggle();
{
self.toggle();
}
};
let show_previous = |ui: &mut Ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(LayoutJob::simple(
egui_phosphor::regular::SKIP_BACK.to_string(),
icon_font_id.clone(),
prev_color,
100.0,
))
.selectable(false),
)
})
.clicked()
&& is_previous_enabled
{
self.previous();
}
};
let show_play_pause = |ui: &mut Ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(LayoutJob::simple(
play_pause_icon.to_string(),
icon_font_id.clone(),
text_color,
100.0,
))
.selectable(false),
)
})
.on_hover_text(&output)
.clicked()
{
self.toggle();
}
};
let show_next = |ui: &mut Ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(LayoutJob::simple(
egui_phosphor::regular::SKIP_FORWARD.to_string(),
icon_font_id.clone(),
next_color,
100.0,
))
.selectable(false),
)
})
.clicked()
&& is_next_enabled
{
self.next();
}
};
config.apply_on_widget(false, ui, |ui| {
if is_reversed {
// Right panel renders right-to-left, so reverse order
if show_controls {
show_next(ui);
show_play_pause(ui);
show_previous(ui);
}
});
}
show_label(ui);
} else {
// Left/center panel renders left-to-right, normal order
show_label(ui);
if show_controls {
show_previous(ui);
show_play_pause(ui);
show_next(ui);
}
}
});
}
}
}

View File

@@ -20,6 +20,8 @@ pub mod media;
pub mod memory;
pub mod network;
pub mod storage;
#[cfg(target_os = "windows")]
pub mod systray;
pub mod time;
pub mod update;
pub mod widget;
@@ -92,10 +94,16 @@ impl IconsCache {
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
self.images.write().unwrap().insert(id, image);
}
/// Removes the cached image and texture for the given icon ID.
pub fn remove(&self, id: &ImageIconId) {
self.images.write().unwrap().remove(id);
self.textures.write().unwrap().1.remove(id);
}
}
#[inline]
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
pub(crate) 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())
@@ -156,6 +164,8 @@ pub enum ImageIconId {
Path(Arc<Path>),
/// Windows HWND handle.
Hwnd(isize),
/// System tray icon identifier.
SystrayIcon(String),
}
impl From<&Path> for ImageIconId {

View File

@@ -33,6 +33,8 @@ pub struct StorageConfig {
/// Show removable disks
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub show_removable_disks: Option<bool>,
/// Storage display name
pub storage_display_name: Option<StorageDisplayName>,
/// 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]]
@@ -48,6 +50,9 @@ impl From<StorageConfig> for Storage {
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),
storage_display_name: value
.storage_display_name
.unwrap_or(StorageDisplayName::Mount),
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(),
@@ -55,6 +60,19 @@ impl From<StorageConfig> for Storage {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum StorageDisplayName {
/// Display label as mount point eg. C:\
Mount,
/// Display label as name eg. Local Disk
Name,
/// Display label as mount then name eg. C:\ Local Disk
MountAndName,
/// Display label as name then mount eg. Local Disk C:\
NameAndMount,
}
struct StorageDisk {
label: String,
selected: bool,
@@ -67,6 +85,7 @@ pub struct Storage {
label_prefix: LabelPrefix,
show_read_only_disks: bool,
show_removable_disks: bool,
storage_display_name: StorageDisplayName,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant,
@@ -90,6 +109,17 @@ impl Storage {
continue;
}
let mount = disk.mount_point();
let name = disk.name();
let display_name = match self.storage_display_name {
StorageDisplayName::Mount => mount.to_string_lossy(),
StorageDisplayName::Name => name.to_string_lossy(),
StorageDisplayName::MountAndName => {
mount.to_string_lossy() + name.to_string_lossy()
}
StorageDisplayName::NameAndMount => {
name.to_string_lossy() + mount.to_string_lossy()
}
};
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
@@ -103,7 +133,7 @@ impl Storage {
disks.push(StorageDisk {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), percentage)
format!("{} {}%", display_name, percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
},

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,10 @@ use crate::widgets::network::Network;
use crate::widgets::network::NetworkConfig;
use crate::widgets::storage::Storage;
use crate::widgets::storage::StorageConfig;
#[cfg(target_os = "windows")]
use crate::widgets::systray::Systray;
#[cfg(target_os = "windows")]
use crate::widgets::systray::SystrayConfig;
use crate::widgets::time::Time;
use crate::widgets::time::TimeConfig;
use crate::widgets::update::Update;
@@ -66,6 +70,10 @@ pub enum WidgetConfig {
/// Storage widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
Storage(StorageConfig),
/// System Tray widget configuration (Windows only)
#[cfg(target_os = "windows")]
#[cfg_attr(feature = "schemars", schemars(title = "Systray"))]
Systray(SystrayConfig),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig),
@@ -87,6 +95,8 @@ impl WidgetConfig {
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
#[cfg(target_os = "windows")]
WidgetConfig::Systray(config) => Box::new(Systray::from(config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
}
@@ -112,6 +122,8 @@ impl WidgetConfig {
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
#[cfg(target_os = "windows")]
WidgetConfig::Systray(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
WidgetConfig::Update(config) => config.enable,
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-layouts"
version = "0.1.40"
version = "0.1.41"
edition = "2024"
[dependencies]

View File

@@ -141,6 +141,15 @@ impl Arrangement for DefaultLayout {
});
}
// Last visible column absorbs any remainder from integer division
// so that visible columns tile the full area width without gaps
let width_remainder = area.right - column_width * visible_columns;
if width_remainder > 0 {
let last_visible_idx =
(first_visible as usize + visible_columns as usize - 1).min(len - 1);
layouts[last_visible_idx].right += width_remainder;
}
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
layouts
.iter_mut()
@@ -660,6 +669,34 @@ impl Arrangement for DefaultLayout {
current_left += width;
}
// Last column absorbs any remainder from integer division
// so that columns tile the full area width without gaps
let total_width: i32 = col_widths.iter().sum();
let width_remainder = area.right - total_width;
if width_remainder > 0
&& let Some(last) = col_widths.last_mut()
{
*last += width_remainder;
}
// Pre-calculate flipped column positions: same widths laid out
// in reverse order so that the last column sits at area.left
let flipped_col_lefts = if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
let n = num_cols as usize;
let mut flipped = vec![0i32; n];
let mut fl = area.left;
for i in (0..n).rev() {
flipped[i] = fl;
fl += col_widths[i];
}
flipped
} else {
vec![]
};
let mut iter = layouts.iter_mut().enumerate().peekable();
for col in 0..num_cols {
@@ -673,8 +710,10 @@ impl Arrangement for DefaultLayout {
remaining_windows / remaining_columns
};
// Rows within each column are equal height (no row_ratios support for Grid)
let win_height = area.bottom / num_rows_in_this_col;
// Rows within each column: base height from integer division,
// last row absorbs any remainder to cover the full area height
let base_height = area.bottom / num_rows_in_this_col;
let height_remainder = area.bottom - base_height * num_rows_in_this_col;
let col_idx = col as usize;
let win_width = col_widths[col_idx];
@@ -682,25 +721,36 @@ impl Arrangement for DefaultLayout {
for row in 0..num_rows_in_this_col {
if let Some((_idx, win)) = iter.next() {
let is_last_row = row == num_rows_in_this_col - 1;
let win_height = if is_last_row {
base_height + height_remainder
} else {
base_height
};
let mut left = col_left;
let mut top = area.top + win_height * row;
let mut top = area.top + base_height * row;
match layout_flip {
Some(Axis::Horizontal) => {
// Calculate flipped left position
let flipped_col = (num_cols - 1 - col) as usize;
left = col_lefts[flipped_col];
left = flipped_col_lefts[col_idx];
}
Some(Axis::Vertical) => {
// Calculate flipped top position
top = area.bottom - win_height * (row + 1) + area.top;
top = if is_last_row {
area.top
} else {
area.top + area.bottom - base_height * (row + 1)
};
}
Some(Axis::HorizontalAndVertical) => {
let flipped_col = (num_cols - 1 - col) as usize;
left = col_lefts[flipped_col];
top = area.bottom - win_height * (row + 1) + area.top;
left = flipped_col_lefts[col_idx];
top = if is_last_row {
area.top
} else {
area.top + area.bottom - base_height * (row + 1)
};
}
None => {} // No flip
None => {}
}
win.bottom = win_height;
@@ -934,6 +984,16 @@ fn columns_with_ratios(
left += right;
}
// Last column absorbs any remainder from integer division
// so that columns tile the full area width without gaps
let total_width: i32 = layouts.iter().map(|r| r.right).sum();
let remainder = area.right - total_width;
if remainder > 0
&& let Some(last) = layouts.last_mut()
{
last.right += remainder;
}
layouts
}
@@ -1005,6 +1065,16 @@ fn rows_with_ratios(
top += bottom;
}
// Last row absorbs any remainder from integer division
// so that rows tile the full area height without gaps
let total_height: i32 = layouts.iter().map(|r| r.bottom).sum();
let remainder = area.bottom - total_height;
if remainder > 0
&& let Some(last) = layouts.last_mut()
{
last.bottom += remainder;
}
layouts
}
@@ -1129,47 +1199,33 @@ fn recursive_fibonacci(
*area
};
#[allow(clippy::cast_possible_truncation)]
let primary_width = (area.right as f32 * column_split_ratio) as i32;
#[allow(clippy::cast_possible_truncation)]
let primary_height = (area.bottom as f32 * row_split_ratio) as i32;
#[allow(clippy::cast_possible_truncation)]
let primary_resized_width = (resized.right as f32 * column_split_ratio) as i32;
#[allow(clippy::cast_possible_truncation)]
let primary_resized_height = (resized.bottom as f32 * row_split_ratio) as i32;
let secondary_width = area.right - primary_width;
let secondary_resized_width = resized.right - primary_resized_width;
let secondary_resized_height = resized.bottom - primary_resized_height;
let (main_x, alt_x, alt_y, main_y);
if let Some(flip) = layout_flip {
match flip {
Axis::Horizontal => {
main_x =
resized.left + secondary_width + (secondary_width - secondary_resized_width);
main_x = resized.left + (area.right - primary_resized_width);
alt_x = resized.left;
alt_y = resized.top + primary_resized_height;
main_y = resized.top;
}
Axis::Vertical => {
main_y = resized.top
+ (area.bottom - primary_height)
+ ((area.bottom - primary_height) - secondary_resized_height);
main_y = resized.top + (area.bottom - primary_resized_height);
alt_y = resized.top;
main_x = resized.left;
alt_x = resized.left + primary_resized_width;
}
Axis::HorizontalAndVertical => {
main_x =
resized.left + secondary_width + (secondary_width - secondary_resized_width);
main_x = resized.left + (area.right - primary_resized_width);
alt_x = resized.left;
main_y = resized.top
+ (area.bottom - primary_height)
+ ((area.bottom - primary_height) - secondary_resized_height);
main_y = resized.top + (area.bottom - primary_resized_height);
alt_y = resized.top;
}
}
@@ -1541,657 +1597,5 @@ fn resize_bottom(rect: &mut Rect, resize: i32) {
}
#[cfg(test)]
mod tests {
use super::*;
use std::num::NonZeroUsize;
// Helper to create a test area
fn test_area() -> Rect {
Rect {
left: 0,
top: 0,
right: 1000,
bottom: 800,
}
}
// Helper to create LayoutOptions with column ratios
fn layout_options_with_column_ratios(ratios: &[f32]) -> LayoutOptions {
let mut arr = [None; MAX_RATIOS];
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: Some(arr),
row_ratios: None,
}
}
// Helper to create LayoutOptions with row ratios
fn layout_options_with_row_ratios(ratios: &[f32]) -> LayoutOptions {
let mut arr = [None; MAX_RATIOS];
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: None,
row_ratios: Some(arr),
}
}
// Helper to create LayoutOptions with both column and row ratios
fn layout_options_with_ratios(column_ratios: &[f32], row_ratios: &[f32]) -> LayoutOptions {
let mut col_arr = [None; MAX_RATIOS];
for (i, &r) in column_ratios.iter().take(MAX_RATIOS).enumerate() {
col_arr[i] = Some(r);
}
let mut row_arr = [None; MAX_RATIOS];
for (i, &r) in row_ratios.iter().take(MAX_RATIOS).enumerate() {
row_arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: Some(col_arr),
row_ratios: Some(row_arr),
}
}
mod columns_with_ratios_tests {
use super::*;
#[test]
fn test_columns_equal_width_no_ratios() {
let area = test_area();
let layouts = columns_with_ratios(&area, 4, None);
assert_eq!(layouts.len(), 4);
// Each column should be 250 pixels wide (1000 / 4)
for layout in &layouts {
assert_eq!(layout.right, 250);
assert_eq!(layout.bottom, 800);
}
}
#[test]
fn test_columns_with_single_ratio() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts = columns_with_ratios(&area, 3, opts.column_ratios);
assert_eq!(layouts.len(), 3);
// First column: 30% of 1000 = 300
assert_eq!(layouts[0].right, 300);
// Remaining 700 split between 2 columns = 350 each
assert_eq!(layouts[1].right, 350);
assert_eq!(layouts[2].right, 350);
}
#[test]
fn test_columns_with_multiple_ratios() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.2, 0.3, 0.5]);
let layouts = columns_with_ratios(&area, 4, opts.column_ratios);
assert_eq!(layouts.len(), 4);
// First column: 20% of 1000 = 200
assert_eq!(layouts[0].right, 200);
// Second column: 30% of 1000 = 300
assert_eq!(layouts[1].right, 300);
// Third column: 50% of 1000 = 500
// But wait - cumulative is 1.0, so third might be truncated
// Let's check what actually happens
// Actually, the sum 0.2 + 0.3 = 0.5 < 1.0, and 0.5 + 0.5 = 1.0
// So 0.5 won't be included because cumulative would reach 1.0
}
#[test]
fn test_columns_positions_are_correct() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3, 0.4]);
let layouts = columns_with_ratios(&area, 3, opts.column_ratios);
// First column starts at 0
assert_eq!(layouts[0].left, 0);
// Second column starts where first ends
assert_eq!(layouts[1].left, layouts[0].right);
// Third column starts where second ends
assert_eq!(layouts[2].left, layouts[1].left + layouts[1].right);
}
#[test]
fn test_columns_last_column_gets_remaining_space() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts = columns_with_ratios(&area, 2, opts.column_ratios);
assert_eq!(layouts.len(), 2);
// First column: 30% = 300
assert_eq!(layouts[0].right, 300);
// Last column gets remaining space: 700
assert_eq!(layouts[1].right, 700);
}
#[test]
fn test_columns_single_column() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.5]);
let layouts = columns_with_ratios(&area, 1, opts.column_ratios);
assert_eq!(layouts.len(), 1);
// Single column takes full width regardless of ratio
assert_eq!(layouts[0].right, 1000);
}
#[test]
fn test_columns_more_columns_than_ratios() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.2]);
let layouts = columns_with_ratios(&area, 5, opts.column_ratios);
assert_eq!(layouts.len(), 5);
// First column: 20% = 200
assert_eq!(layouts[0].right, 200);
// Remaining 800 split among 4 columns = 200 each
for i in 1..5 {
assert_eq!(layouts[i].right, 200);
}
}
}
mod rows_with_ratios_tests {
use super::*;
#[test]
fn test_rows_equal_height_no_ratios() {
let area = test_area();
let layouts = rows_with_ratios(&area, 4, None);
assert_eq!(layouts.len(), 4);
// Each row should be 200 pixels tall (800 / 4)
for layout in &layouts {
assert_eq!(layout.bottom, 200);
assert_eq!(layout.right, 1000);
}
}
#[test]
fn test_rows_with_single_ratio() {
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.5]);
let layouts = rows_with_ratios(&area, 3, opts.row_ratios);
assert_eq!(layouts.len(), 3);
// First row: 50% of 800 = 400
assert_eq!(layouts[0].bottom, 400);
// Remaining 400 split between 2 rows = 200 each
assert_eq!(layouts[1].bottom, 200);
assert_eq!(layouts[2].bottom, 200);
}
#[test]
fn test_rows_positions_are_correct() {
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.25, 0.25]);
let layouts = rows_with_ratios(&area, 3, opts.row_ratios);
// First row starts at top
assert_eq!(layouts[0].top, 0);
// Second row starts where first ends
assert_eq!(layouts[1].top, layouts[0].bottom);
// Third row starts where second ends
assert_eq!(layouts[2].top, layouts[1].top + layouts[1].bottom);
}
#[test]
fn test_rows_last_row_gets_remaining_space() {
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.25]);
let layouts = rows_with_ratios(&area, 2, opts.row_ratios);
assert_eq!(layouts.len(), 2);
// First row: 25% of 800 = 200
assert_eq!(layouts[0].bottom, 200);
// Last row gets remaining: 600
assert_eq!(layouts[1].bottom, 600);
}
}
mod vertical_stack_layout_tests {
use super::*;
#[test]
fn test_vertical_stack_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts =
DefaultLayout::VerticalStack.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 3);
// Primary column should be 50% (default ratio)
assert_eq!(layouts[0].right, 500);
}
#[test]
fn test_vertical_stack_custom_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.7]);
let layouts = DefaultLayout::VerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Primary column should be 70%
assert_eq!(layouts[0].right, 700);
// Stack columns should share remaining 30%
assert_eq!(layouts[1].right, 300);
assert_eq!(layouts[2].right, 300);
}
#[test]
fn test_vertical_stack_with_row_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let opts = layout_options_with_ratios(&[0.6], &[0.5, 0.3]);
let layouts = DefaultLayout::VerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 4);
// Primary column: 60%
assert_eq!(layouts[0].right, 600);
// Stack rows should use row_ratios
// First stack row: 50% of 800 = 400
assert_eq!(layouts[1].bottom, 400);
// Second stack row: 30% of 800 = 240
assert_eq!(layouts[2].bottom, 240);
}
#[test]
fn test_vertical_stack_single_window() {
let area = test_area();
let len = NonZeroUsize::new(1).unwrap();
let opts = layout_options_with_column_ratios(&[0.6]);
let layouts = DefaultLayout::VerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 1);
// Single window should take full width
assert_eq!(layouts[0].right, 1000);
}
}
mod horizontal_stack_layout_tests {
use super::*;
#[test]
fn test_horizontal_stack_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts =
DefaultLayout::HorizontalStack.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 3);
// Primary row should be 50% height (default ratio)
assert_eq!(layouts[0].bottom, 400);
}
#[test]
fn test_horizontal_stack_custom_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_row_ratios(&[0.7]);
let layouts = DefaultLayout::HorizontalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Primary row should be 70% height
assert_eq!(layouts[0].bottom, 560);
}
}
mod ultrawide_layout_tests {
use super::*;
#[test]
fn test_ultrawide_default_ratios() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
None,
&[],
);
assert_eq!(layouts.len(), 3);
// Primary (center): 50% = 500
assert_eq!(layouts[0].right, 500);
// Secondary (left): 25% = 250
assert_eq!(layouts[1].right, 250);
// Tertiary gets remaining: 250
assert_eq!(layouts[2].right, 250);
}
#[test]
fn test_ultrawide_custom_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let opts = layout_options_with_column_ratios(&[0.5, 0.2]);
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 4);
// Primary (center): 50% = 500
assert_eq!(layouts[0].right, 500);
// Secondary (left): 20% = 200
assert_eq!(layouts[1].right, 200);
// Tertiary column gets remaining: 300
assert_eq!(layouts[2].right, 300);
assert_eq!(layouts[3].right, 300);
}
#[test]
fn test_ultrawide_two_windows() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let opts = layout_options_with_column_ratios(&[0.6]);
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 2);
// Primary: 60% = 600
assert_eq!(layouts[0].right, 600);
// Secondary gets remaining: 400
assert_eq!(layouts[1].right, 400);
}
}
mod bsp_layout_tests {
use super::*;
#[test]
fn test_bsp_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let layouts = DefaultLayout::BSP.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 2);
// First window should be 50% width
assert_eq!(layouts[0].right, 500);
}
#[test]
fn test_bsp_custom_column_ratio() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let opts = layout_options_with_column_ratios(&[0.7]);
let layouts =
DefaultLayout::BSP.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 2);
// First window should be 70% width
assert_eq!(layouts[0].right, 700);
}
#[test]
fn test_bsp_custom_row_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_ratios(&[0.5], &[0.7]);
let layouts =
DefaultLayout::BSP.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 3);
// Second window should be 70% of remaining height
assert_eq!(layouts[1].bottom, 560);
}
}
mod right_main_vertical_stack_tests {
use super::*;
#[test]
fn test_right_main_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts = DefaultLayout::RightMainVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
None,
&[],
);
assert_eq!(layouts.len(), 3);
// Primary should be on the right, 50% width
assert_eq!(layouts[0].right, 500);
assert_eq!(layouts[0].left, 500); // Right side
}
#[test]
fn test_right_main_custom_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.6]);
let layouts = DefaultLayout::RightMainVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Primary: 60% = 600
assert_eq!(layouts[0].right, 600);
// Should be positioned on the right
assert_eq!(layouts[0].left, 400);
}
}
mod columns_layout_tests {
use super::*;
#[test]
fn test_columns_layout_with_ratios() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.2, 0.5]);
let layouts =
DefaultLayout::Columns.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 3);
assert_eq!(layouts[0].right, 200); // 20%
assert_eq!(layouts[1].right, 500); // 50%
assert_eq!(layouts[2].right, 300); // remaining
}
}
mod rows_layout_tests {
use super::*;
#[test]
fn test_rows_layout_with_ratios() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_row_ratios(&[0.25, 0.5]);
let layouts =
DefaultLayout::Rows.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 3);
assert_eq!(layouts[0].bottom, 200); // 25%
assert_eq!(layouts[1].bottom, 400); // 50%
assert_eq!(layouts[2].bottom, 200); // remaining
}
}
mod grid_layout_tests {
use super::*;
#[test]
fn test_grid_with_column_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts =
DefaultLayout::Grid.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 4);
// Grid with 4 windows should be 2x2
// First column: 30% = 300
assert_eq!(layouts[0].right, 300);
assert_eq!(layouts[1].right, 300);
}
#[test]
fn test_grid_without_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let layouts = DefaultLayout::Grid.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 4);
// 2x2 grid, equal columns = 500 each
assert_eq!(layouts[0].right, 500);
assert_eq!(layouts[2].right, 500);
}
}
mod layout_flip_tests {
use super::*;
#[test]
fn test_columns_flip_horizontal() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.2, 0.3]);
let layouts = DefaultLayout::Columns.calculate(
&area,
len,
None,
Some(Axis::Horizontal),
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Columns should be reversed
// Last column (originally 50%) should now be first
assert_eq!(layouts[2].left, 0);
}
#[test]
fn test_rows_flip_vertical() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_row_ratios(&[0.25, 0.5]);
let layouts = DefaultLayout::Rows.calculate(
&area,
len,
None,
Some(Axis::Vertical),
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Rows should be reversed
// Last row should now be at top
assert_eq!(layouts[2].top, 0);
}
}
mod container_padding_tests {
use super::*;
#[test]
fn test_padding_applied_to_all_layouts() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let padding = 10;
let layouts = DefaultLayout::Columns.calculate(
&area,
len,
Some(padding),
None,
&[],
0,
None,
&[],
);
assert_eq!(layouts.len(), 2);
// Each layout should have padding applied
// left increases, right decreases, top increases, bottom decreases
assert_eq!(layouts[0].left, padding);
assert_eq!(layouts[0].top, padding);
assert_eq!(layouts[0].right, 500 - padding * 2);
assert_eq!(layouts[0].bottom, 800 - padding * 2);
}
}
}
#[path = "arrangement_tests.rs"]
mod tests;

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,36 @@ pub const DEFAULT_RATIO: f32 = 0.5;
/// Default secondary ratio value for UltrawideVerticalStack layout
pub const DEFAULT_SECONDARY_RATIO: f32 = 0.25;
/// Validates and converts a Vec of ratios into a fixed-size array.
/// - Clamps values to MIN_RATIO..MAX_RATIO range
/// - Truncates when cumulative sum reaches or exceeds 1.0
/// - Limits to MAX_RATIOS values
#[must_use]
pub fn validate_ratios(ratios: &[f32]) -> [Option<f32>; MAX_RATIOS] {
let mut arr = [None; MAX_RATIOS];
let mut cumulative_sum = 0.0_f32;
for (i, &val) in ratios.iter().take(MAX_RATIOS).enumerate() {
let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO);
// Only add this ratio if cumulative sum stays below 1.0
if cumulative_sum + clamped_val < 1.0 {
arr[i] = Some(clamped_val);
cumulative_sum += clamped_val;
} else {
// Stop adding ratios - cumulative sum would reach or exceed 1.0
tracing::debug!(
"Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0",
i,
cumulative_sum,
clamped_val
);
break;
}
}
arr
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
)]
@@ -137,30 +167,7 @@ where
D: serde::Deserializer<'de>,
{
let opt: Option<Vec<f32>> = Option::deserialize(deserializer)?;
Ok(opt.map(|vec| {
let mut arr = [None; MAX_RATIOS];
let mut cumulative_sum = 0.0_f32;
for (i, &val) in vec.iter().take(MAX_RATIOS).enumerate() {
let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO);
// Only add this ratio if cumulative sum stays below 1.0
if cumulative_sum + clamped_val < 1.0 {
arr[i] = Some(clamped_val);
cumulative_sum += clamped_val;
} else {
// Stop adding ratios - cumulative sum would reach or exceed 1.0
tracing::debug!(
"Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0",
i,
cumulative_sum,
clamped_val
);
break;
}
}
arr
}))
Ok(opt.map(|vec| validate_ratios(&vec)))
}
/// Helper to serialize [Option<f32>; MAX_RATIOS] as a compact array (without trailing nulls)
@@ -521,8 +528,8 @@ mod tests {
let ratios = opts.column_ratios.unwrap();
// Only MAX_RATIOS (5) values should be stored
for i in 0..MAX_RATIOS {
assert_eq!(ratios[i], Some(0.1));
for item in ratios.iter().take(MAX_RATIOS) {
assert_eq!(*item, Some(0.1));
}
}
@@ -532,8 +539,8 @@ mod tests {
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.column_ratios.unwrap();
for i in 0..MAX_RATIOS {
assert_eq!(ratios[i], None);
for item in ratios.iter().take(MAX_RATIOS) {
assert_eq!(*item, None);
}
}
@@ -626,12 +633,16 @@ mod tests {
#[test]
fn test_constants_valid_ranges() {
assert!(MIN_RATIO > 0.0);
assert!(MIN_RATIO < MAX_RATIO);
assert!(MAX_RATIO < 1.0);
assert!(DEFAULT_RATIO >= MIN_RATIO && DEFAULT_RATIO <= MAX_RATIO);
assert!(DEFAULT_SECONDARY_RATIO >= MIN_RATIO && DEFAULT_SECONDARY_RATIO <= MAX_RATIO);
assert!(MAX_RATIOS >= 1);
const {
assert!(MIN_RATIO > 0.0);
assert!(MIN_RATIO < MAX_RATIO);
assert!(MAX_RATIO < 1.0);
assert!(DEFAULT_RATIO >= MIN_RATIO && DEFAULT_RATIO <= MAX_RATIO);
assert!(
DEFAULT_SECONDARY_RATIO >= MIN_RATIO && DEFAULT_SECONDARY_RATIO <= MAX_RATIO
);
assert!(MAX_RATIOS >= 1);
}
}
#[test]

View File

@@ -1,10 +1,10 @@
[package]
name = "komorebi-themes"
version = "0.1.40"
version = "0.1.41"
edition = "2024"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "b9e26b31f7a0e7ed239b14e5317e95d1bdc544bd" }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "3f157904c641f0dc80f043449fe0214fc4182425" }
#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",

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.40"
version = "0.1.41"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2024"
@@ -51,7 +51,7 @@ windows-numerics = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.55"
winreg = "0.56"
serde_with = { version = "3.12", features = ["schemars_1"] }
[build-dependencies]

View File

@@ -58,6 +58,7 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
@@ -67,11 +68,16 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
use windows::Win32::UI::WindowsAndMessaging::WM_USER;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows_core::BOOL;
use windows_core::PCWSTR;
use windows_numerics::Matrix3x2;
/// Custom WM_USER message that tells the border window thread to call update_brushes() on itself,
/// avoiding a data race between the border manager thread and the border's message loop thread.
pub const WM_UPDATE_BRUSHES: u32 = WM_USER + 1;
pub struct RenderFactory(ID2D1Factory);
unsafe impl Sync for RenderFactory {}
unsafe impl Send for RenderFactory {}
@@ -318,20 +324,31 @@ impl Border {
}
pub fn destroy(&self) -> color_eyre::Result<()> {
// signal that we're destroying - prevents new render operations
// signal that we're destroying - prevents new render operations from starting
self.is_destroying.store(true, Ordering::Release);
// small delay to allow in-flight render operations to complete
std::thread::sleep(std::time::Duration::from_millis(10));
// clear user data **BEFORE** closing window
// pending messages will see a null pointer and exit early
unsafe {
SetWindowLongPtrW(self.hwnd(), GWLP_USERDATA, 0);
}
// WM_DESTROY will clear GWLP_USERDATA and drop the render target before D2D
// frees its internal HwndPresenter during WM_NCDESTROY
WindowsApi::close_window(self.hwnd)
}
/// Post a message to the border's own message loop thread requesting a brush update.
/// This ensures update_brushes() always runs on the window thread that owns the D2D
/// render target, preventing a data race with concurrent WndProc render operations.
pub fn request_brush_update(&self) {
let _ = unsafe {
PostMessageW(
Option::from(self.hwnd()),
WM_UPDATE_BRUSHES,
WPARAM(0),
LPARAM(0),
)
};
}
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
let mut rect = *rect;
rect.add_margin(self.width);
@@ -576,6 +593,20 @@ impl Border {
let _ = ValidateRect(Option::from(window), None);
LRESULT(0)
}
WM_UPDATE_BRUSHES => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
if let Err(error) = (*border_pointer).update_brushes() {
tracing::error!("failed to update brushes: {error}");
}
(*border_pointer).invalidate();
LRESULT(0)
}
WM_DESTROY => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if !border_pointer.is_null() {

View File

@@ -451,8 +451,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} else if matches!(notification, Notification::ForceUpdate) {
// Update the border brushes if there was a forced update
// notification and this is not a new border (new border's
// already have their brushes updated on creation)
border.update_brushes()?;
// already have their brushes updated on creation).
// Post to the border's own thread to avoid a data race between
// this thread dropping the old render target and the window
// thread mid-render holding a reference to it.
border.request_brush_update();
}
border.invalidate();
@@ -616,8 +619,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if forced_update && !new_border {
// Update the border brushes if there was a forced update
// notification and this is not a new border (new border's
// already have their brushes updated on creation)
border.update_brushes()?;
// already have their brushes updated on creation).
// Post to the border's own thread to avoid a data race between
// this thread dropping the old render target and the window
// thread mid-render holding a reference to it.
border.request_brush_update();
}
border.set_position(&rect, focused_window_hwnd)?;
border.invalidate();
@@ -699,8 +705,11 @@ fn handle_floating_borders(
if forced_update && !new_border {
// Update the border brushes if there was a forced update
// notification and this is not a new border (new border's
// already have their brushes updated on creation)
border.update_brushes()?;
// already have their brushes updated on creation).
// Post to the border's own thread to avoid a data race between
// this thread dropping the old render target and the window
// thread mid-render holding a reference to it.
border.request_brush_update();
}
border.set_position(&rect, window.hwnd)?;
border.invalidate();

View File

@@ -74,7 +74,7 @@ pub enum AnimationStyle {
EaseInOutBounce,
#[cfg_attr(feature = "schemars", schemars(title = "CubicBezier"))]
#[value(skip)]
/// Custom Cubic Bézier function
/// Custom Cubic Bezier function
CubicBezier(f64, f64, f64, f64),
}

View File

@@ -15,6 +15,7 @@ use strum::EnumString;
use crate::KomorebiTheme;
use crate::animation::prefix::AnimationPrefix;
use crate::state::State;
// Re-export everything from komorebi-layouts
pub use komorebi_layouts::Arrangement;
@@ -39,6 +40,7 @@ pub use komorebi_layouts::OperationDirection;
pub use komorebi_layouts::Rect;
pub use komorebi_layouts::ScrollingLayoutOptions;
pub use komorebi_layouts::Sizing;
pub use komorebi_layouts::validate_ratios;
// Local modules and exports
pub use animation::AnimationStyle;
@@ -118,6 +120,7 @@ pub enum SocketMessage {
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(DefaultLayout),
CycleLayout(CycleDirection),
LayoutRatios(Option<Vec<f32>>, Option<Vec<f32>>),
ScrollingLayoutColumns(NonZeroUsize),
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
FlipLayout(Axis),
@@ -254,6 +257,8 @@ pub enum SocketMessage {
StaticConfigSchema,
GenerateStaticConfig,
DebugWindow(isize),
// low level commands
ApplyState(State),
}
impl SocketMessage {

View File

@@ -322,7 +322,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
// the latter case, if the user desires this validation after initiating the task view, komorebi
// should be restarted, and then when this // fn runs again for the first time, it will pick up
// the value of CurrentVirtualDesktop and validate against it accordingly
current
current.map(|current| current.to_vec())
}
#[derive(Clone, Debug, Serialize, Deserialize)]

View File

@@ -957,6 +957,29 @@ impl WindowManager {
}
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
SocketMessage::LayoutRatios(ref columns, ref rows) => {
use crate::core::validate_ratios;
let focused_workspace = self.focused_workspace_mut()?;
let mut options = focused_workspace.layout_options.unwrap_or(LayoutOptions {
scrolling: None,
grid: None,
column_ratios: None,
row_ratios: None,
});
if let Some(cols) = columns {
options.column_ratios = Some(validate_ratios(cols));
}
if let Some(rws) = rows {
options.row_ratios = Some(validate_ratios(rws));
}
focused_workspace.layout_options = Some(options);
self.update_focused_workspace(false, false)?;
}
SocketMessage::ChangeLayoutCustom(ref path) => {
self.change_workspace_custom_layout(path)?;
}
@@ -2273,6 +2296,9 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
SocketMessage::Theme(ref theme) => {
theme_manager::send_notification(*theme.clone());
}
SocketMessage::ApplyState(ref state) => {
self.apply_state(state.clone());
}
// Deprecated commands
SocketMessage::AltFocusHack(_)
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}

View File

@@ -253,6 +253,7 @@ impl From<&WindowManager> for State {
layout: workspace.layout.clone(),
layout_options: workspace.layout_options,
layout_rules: workspace.layout_rules.clone(),
work_area_offset_rules: workspace.work_area_offset_rules.clone(),
layout_flip: workspace.layout_flip,
workspace_padding: workspace.workspace_padding,
container_padding: workspace.container_padding,

View File

@@ -223,6 +223,9 @@ pub struct WorkspaceConfig {
/// Layout rules in the format of threshold => layout
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
/// Work area offset rules in the format of threshold => Rect (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub work_area_offset_rules: Option<HashMap<usize, Rect>>,
/// END OF LIFE FEATURE: Custom layout rules
#[deprecated(note = "End of life feature")]
#[serde(skip_serializing_if = "Option::is_none")]
@@ -287,6 +290,13 @@ impl From<&Workspace> for WorkspaceConfig {
}
let layout_rules = (!layout_rules.is_empty()).then_some(layout_rules);
let mut work_area_offset_rules = HashMap::new();
for (threshold, offset) in &value.work_area_offset_rules {
work_area_offset_rules.insert(*threshold, *offset);
}
let work_area_offset_rules =
(!work_area_offset_rules.is_empty()).then_some(work_area_offset_rules);
let mut window_container_behaviour_rules = HashMap::new();
for (threshold, behaviour) in value.window_container_behaviour_rules.iter().flatten() {
window_container_behaviour_rules.insert(*threshold, *behaviour);
@@ -353,6 +363,7 @@ impl From<&Workspace> for WorkspaceConfig {
.workspace_config
.as_ref()
.and_then(|c| c.workspace_rules.clone()),
work_area_offset_rules,
work_area_offset: value.work_area_offset,
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset),
window_container_behaviour: value.window_container_behaviour,
@@ -451,7 +462,7 @@ pub enum AppSpecificConfigurationPath {
#[serde_with::serde_as]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.json` static configuration file reference for `v0.1.40`
/// The `komorebi.json` static configuration file reference for `v0.1.41`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[deprecated(note = "No longer required")]
@@ -1919,7 +1930,7 @@ mod tests {
let docs = vec![
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
"0.1.36", "0.1.37", "0.1.38",
"0.1.36", "0.1.37", "0.1.38", "0.1.39",
];
let mut versions = vec![];

View File

@@ -239,23 +239,30 @@ impl WindowManager {
let mouse_follows_focus = self.mouse_follows_focus;
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
let mut focused_workspace = 0;
for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx)
&& let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) {
monitor
.workspaces_mut()
.resize(state_monitor.workspaces().len(), Workspace::default());
for (workspace_idx, workspace) in
monitor.workspaces_mut().iter_mut().enumerate()
{
// to make sure padding and layout_options changes get applied for users after a quick restart
let container_padding = workspace.container_padding;
let workspace_padding = workspace.workspace_padding;
let layout_options = workspace.layout_options;
if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
{
// to make sure padding and layout_options changes get applied for users after a quick restart
let container_padding = workspace.container_padding;
let workspace_padding = workspace.workspace_padding;
let layout_options = workspace.layout_options;
*workspace = state_workspace.clone();
*workspace = state_workspace.clone();
workspace.container_padding = container_padding;
workspace.workspace_padding = workspace_padding;
workspace.layout_options = layout_options;
workspace.container_padding = container_padding;
workspace.workspace_padding = workspace_padding;
workspace.layout_options = layout_options;
if state_monitor.focused_workspace_idx() == workspace_idx {
focused_workspace = workspace_idx;
if state_monitor.focused_workspace_idx() == workspace_idx {
focused_workspace = workspace_idx;
}
}
}
}
@@ -2103,12 +2110,19 @@ impl WindowManager {
tracing::info!("focusing container");
let new_idx =
if workspace.maximized_window.is_some() || workspace.monocle_container.is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
if workspace.monocle_container.is_some() {
let cycle_direction = match direction {
OperationDirection::Left | OperationDirection::Down => CycleDirection::Previous,
OperationDirection::Right | OperationDirection::Up => CycleDirection::Next,
};
return self.cycle_monocle(cycle_direction);
}
let new_idx = if workspace.maximized_window.is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let mut cross_monitor_monocle_or_max = false;
@@ -3093,6 +3107,27 @@ impl WindowManager {
workspace.reintegrate_monocle_container()
}
#[tracing::instrument(skip(self))]
pub fn cycle_monocle(&mut self, direction: CycleDirection) -> eyre::Result<()> {
tracing::info!("cycling monocle container");
if self.focused_workspace()?.containers().is_empty() {
return Ok(());
}
self.focused_workspace_mut()?
.cycle_monocle_container(direction)?;
for container in self.focused_workspace_mut()?.containers_mut() {
container.hide(None);
}
// borders were getting funny during cycles, can't be bothered to root cause it
border_manager::destroy_all_borders()?;
self.update_focused_workspace(true, true)
}
#[tracing::instrument(skip(self))]
pub fn toggle_maximize(&mut self) -> eyre::Result<()> {
self.handle_unmanaged_window_behaviour()?;

View File

@@ -61,6 +61,7 @@ pub struct Workspace {
pub layout: Layout,
pub layout_options: Option<LayoutOptions>,
pub layout_rules: Vec<(usize, Layout)>,
pub work_area_offset_rules: Vec<(usize, Rect)>,
pub layout_flip: Option<Axis>,
pub workspace_padding: Option<i32>,
pub container_padding: Option<i32>,
@@ -118,6 +119,7 @@ impl Default for Workspace {
layout: Layout::Default(DefaultLayout::BSP),
layout_options: None,
layout_rules: vec![],
work_area_offset_rules: vec![],
layout_flip: None,
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
container_padding: Option::from(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)),
@@ -213,6 +215,15 @@ impl Workspace {
self.layout_rules = all_layout_rules;
}
let mut all_work_area_offset_rules = vec![];
if let Some(work_area_offset_rules) = &config.work_area_offset_rules {
for (count, rect) in work_area_offset_rules {
all_work_area_offset_rules.push((*count, *rect));
}
all_work_area_offset_rules.sort_by_key(|(i, _)| *i);
self.work_area_offset_rules = all_work_area_offset_rules;
}
self.work_area_offset = config.work_area_offset;
self.apply_window_based_work_area_offset =
@@ -479,9 +490,27 @@ impl Workspace {
let border_width = self.globals.border_width;
let border_offset = self.globals.border_offset;
let work_area = self.globals.work_area;
let work_area_offset = self.work_area_offset.or(self.globals.work_area_offset);
let window_based_work_area_offset = self.globals.window_based_work_area_offset;
let window_based_work_area_offset_limit = self.globals.window_based_work_area_offset_limit;
let mut rules_work_area_offset = None;
if !self.work_area_offset_rules.is_empty() {
let count = if self.monocle_container.is_some() {
1
} else {
self.containers().len()
};
for (threshold, work_area_offset_rule) in &self.work_area_offset_rules {
if count >= *threshold {
rules_work_area_offset = Some(*work_area_offset_rule);
}
}
};
let work_area_offset = rules_work_area_offset
.or(self.work_area_offset)
.or(self.globals.work_area_offset);
let mut adjusted_work_area = work_area_offset.map_or_else(
|| work_area,
@@ -495,7 +524,6 @@ impl Workspace {
with_offset
},
);
if (self.containers().len() <= window_based_work_area_offset_limit as usize
|| self.monocle_container.is_some() && window_based_work_area_offset_limit > 0)
&& self.apply_window_based_work_area_offset
@@ -1515,6 +1543,23 @@ impl Workspace {
Ok(())
}
pub fn cycle_monocle_container(&mut self, direction: CycleDirection) -> eyre::Result<()> {
if self.containers().is_empty() {
return Ok(());
}
self.reintegrate_monocle_container()?;
let new_idx = self
.new_idx_for_cycle_direction(direction)
.ok_or_eyre("there is no container to cycle monocle to")?;
self.focus_container(new_idx);
self.new_monocle_container()?;
Ok(())
}
pub fn new_maximized_window(&mut self) -> eyre::Result<()> {
let focused_idx = self.focused_container_idx();

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.40"
version = "0.1.41"
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2024"

View File

@@ -1001,6 +1001,16 @@ struct ScrollingLayoutColumns {
count: NonZeroUsize,
}
#[derive(Parser)]
struct LayoutRatios {
/// Column width ratios (space-separated values between 0.1 and 0.9)
#[clap(short, long, num_args = 1..)]
columns: Option<Vec<f32>>,
/// Row height ratios (space-separated values between 0.1 and 0.9)
#[clap(short, long, num_args = 1..)]
rows: Option<Vec<f32>>,
}
#[derive(Parser)]
struct License {
/// Email address associated with an Individual Commercial Use License
@@ -1267,6 +1277,8 @@ enum SubCommand {
/// Set the number of visible columns for the Scrolling layout on the focused workspace
#[clap(arg_required_else_help = true)]
ScrollingLayoutColumns(ScrollingLayoutColumns),
/// Set the layout column and row ratios for the focused workspace
LayoutRatios(LayoutRatios),
/// Load a custom layout from file for the focused workspace
#[clap(hide = true)]
#[clap(arg_required_else_help = true)]
@@ -2934,6 +2946,15 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::ScrollingLayoutColumns(args) => {
send_message(&SocketMessage::ScrollingLayoutColumns(args.count))?;
}
SubCommand::LayoutRatios(args) => {
if args.columns.is_none() && args.rows.is_none() {
println!(
"No ratios provided, nothing to change. Use --columns or --rows to specify ratios."
);
} else {
send_message(&SocketMessage::LayoutRatios(args.columns, args.rows))?;
}
}
SubCommand::LoadCustomLayout(args) => {
send_message(&SocketMessage::ChangeLayoutCustom(args.path))?;
}

View File

@@ -81,6 +81,8 @@ nav:
- common-workflows/mouse-follows-focus.md
- common-workflows/dynamic-layout-switching.md
- common-workflows/multiple-bar-instances.md
- common-workflows/bar.md
- common-workflows/bar-widgets/systray.md
- common-workflows/multi-monitor-setup.md
- CLI reference:
- cli/quickstart.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.40`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.41`",
"type": "object",
"properties": {
"animation": {
@@ -703,7 +703,7 @@
},
{
"title": "CubicBezier",
"description": "Custom Cubic Bézier function",
"description": "Custom Cubic Bezier function",
"type": "object",
"properties": {
"CubicBezier": {
@@ -4286,6 +4286,19 @@
}
]
},
"work_area_offset_rules": {
"description": "Work area offset rules in the format of threshold => Rect (default: None)",
"type": [
"object",
"null"
],
"additionalProperties": false,
"patternProperties": {
"^\\d+$": {
"$ref": "#/$defs/Rect"
}
}
},
"workspace_padding": {
"description": "Workspace padding (default: global)",
"type": [