mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-05-05 18:33:24 +02:00
Compare commits
29 Commits
v0.1.40
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e5bf8c63b | ||
|
|
e2e5dbfcae | ||
|
|
937b28a7d9 | ||
|
|
24c0ce0b1d | ||
|
|
d6b17bbc7c | ||
|
|
e9a541d12b | ||
|
|
588d22a9d2 | ||
|
|
53ad7a2224 | ||
|
|
26b1464381 | ||
|
|
d7580d2271 | ||
|
|
8a1447f543 | ||
|
|
53c81c4596 | ||
|
|
d3779e5a74 | ||
|
|
c84fa50fc9 | ||
|
|
41fc316a59 | ||
|
|
011bcb8bd4 | ||
|
|
dce3c91c22 | ||
|
|
a9a1e68169 | ||
|
|
cb9a7542a6 | ||
|
|
145a0ae003 | ||
|
|
96e87d8ae0 | ||
|
|
529d93595e | ||
|
|
ea35d818a1 | ||
|
|
5f629e1f1a | ||
|
|
0f1854db8b | ||
|
|
8889c3ca93 | ||
|
|
6ca49d4301 | ||
|
|
634a3e7f3b | ||
|
|
5b6fab0044 |
12
.github/workflows/windows.yaml
vendored
12
.github/workflows/windows.yaml
vendored
@@ -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
1
.gitignore
vendored
@@ -12,3 +12,4 @@ komorebic/applications.json
|
||||
/.xwin-cache
|
||||
result
|
||||
/.direnv
|
||||
procdump.exe
|
||||
|
||||
1763
Cargo.lock
generated
1763
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ 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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
"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.101 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"approx 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anyhow 1.0.102 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"approx 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arboard 3.6.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"as-slice 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -41,36 +41,38 @@
|
||||
"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.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",
|
||||
"bitflags 2.11.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitstream-io 4.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"block-buffer 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"by_address 1.2.1 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.56 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cc 1.2.61 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",
|
||||
"chacha20 0.10.0 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.58 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.58 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 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.6.1 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.1 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",
|
||||
"core2 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"colorchoice 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cpufeatures 0.2.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cpufeatures 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crc32fast 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crypto-common 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"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.6 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,33 +97,32 @@
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fast-srgb8 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"filetime 0.2.27 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"find-msvc-tools 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"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",
|
||||
"getrandom 0.1.16 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.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.1 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",
|
||||
"gif 0.14.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gl_generator 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glob 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -133,7 +134,7 @@
|
||||
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.16.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -144,26 +145,26 @@
|
||||
"iana-time-zone 0.1.65 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.25.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.2 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",
|
||||
"imgref 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indenter 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.13.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iri-string 0.7.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.14.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.12 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.182 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.186 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.28 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",
|
||||
@@ -178,15 +179,16 @@
|
||||
"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.16 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",
|
||||
"no_std_io2 0.9.3 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.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-conv 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-conv 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -194,19 +196,18 @@
|
||||
"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",
|
||||
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette_derive 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot 0.12.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot_core 0.9.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pastey 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"percent-encoding 2.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-project-lite 0.2.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.33 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.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -214,37 +215,35 @@
|
||||
"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.30 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.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pxfm 0.1.29 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",
|
||||
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.9.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.9.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon-core 1.13.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ref-cast 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ref-cast-impl 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex 1.12.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",
|
||||
"rustls-pki-types 1.14.1 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",
|
||||
"semver 1.0.28 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_core 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -253,32 +252,32 @@
|
||||
"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",
|
||||
"signature 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 1.0.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.23 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stacker 0.1.24 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",
|
||||
"symlink 0.1.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.115 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.25.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.3 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.4 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",
|
||||
@@ -286,14 +285,14 @@
|
||||
"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",
|
||||
"typenum 1.20.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.23 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-segmentation 1.13.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -301,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.23.1 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.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.2.1 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",
|
||||
@@ -347,7 +347,6 @@
|
||||
"windows-strings 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.60.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.61.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -364,17 +363,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.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.8.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.8.48 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.8.48 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.15 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -396,8 +393,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.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.8.39 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"zerocopy 0.8.48 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.8.48 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -413,9 +410,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.29 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"
|
||||
]
|
||||
],
|
||||
@@ -431,8 +428,9 @@
|
||||
"CC0-1.0",
|
||||
[
|
||||
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fast-srgb8 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"file-id 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"imgref 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"imgref 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"notify 6.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"notify-debouncer-full 0.1.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
@@ -458,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.101 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",
|
||||
@@ -478,12 +476,13 @@
|
||||
"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.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",
|
||||
"bitflags 2.11.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitstream-io 4.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"block-buffer 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli 8.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli-decompressor 5.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"built 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"by_address 1.2.1 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",
|
||||
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -492,38 +491,39 @@
|
||||
"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.56 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cc 1.2.61 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",
|
||||
"chacha20 0.10.0 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.58 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.58 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 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.6.1 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.1 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",
|
||||
"core2 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"colorchoice 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cpufeatures 0.2.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cpufeatures 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crc32fast 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crypto-common 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"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.6 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",
|
||||
@@ -547,41 +547,39 @@
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.33.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equator 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equator-macro 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fax 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fax_derive 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fast-srgb8 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fax 0.2.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"filetime 0.2.27 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"find-msvc-tools 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flate2 1.1.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flavours 0.7.2 git+https://github.com/LGUG2Z/flavours",
|
||||
"flavours 0.7.2 git+https://github.com/LGUG2Z/flavours?rev=24518c129918fe3260aa559eded7657e50752cb1",
|
||||
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"font-loader 0.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"form_urlencoded 1.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fs-tail 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"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.1 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",
|
||||
"gif 0.14.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glob 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -591,7 +589,7 @@
|
||||
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.16.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -600,31 +598,31 @@
|
||||
"http-body 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http-body-util 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper 1.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-util 0.1.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iana-time-zone 0.1.65 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.2 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",
|
||||
"iri-string 0.7.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.14.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.12 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.182 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.186 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.28 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",
|
||||
@@ -644,13 +642,14 @@
|
||||
"miniz_oxide 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mio 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mio 1.2.0 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.16 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.41.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"new_debug_unreachable 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"no_std_io2 0.9.3 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",
|
||||
@@ -660,7 +659,7 @@
|
||||
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-conv 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-conv 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -668,13 +667,13 @@
|
||||
"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",
|
||||
"open 5.3.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"os_info 3.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owo-colors 4.2.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owo-colors 4.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette_derive 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot 0.12.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot_core 0.9.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -682,15 +681,12 @@
|
||||
"percent-encoding 2.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_codegen 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_generator 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_generator 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_macros 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_shared 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_shared 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_shared 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-project-lite 0.2.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.33 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.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -699,42 +695,40 @@
|
||||
"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.30 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"psm 0.1.31 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",
|
||||
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.9.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.9.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"random_word 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon-core 1.13.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ref-cast 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ref-cast-impl 1.0.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex 1.12.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",
|
||||
"rustls-pki-types 1.14.1 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",
|
||||
"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",
|
||||
"semver 1.0.28 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_core 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.228 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -743,47 +737,49 @@
|
||||
"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",
|
||||
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"signature 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"simd-adler32 0.3.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"simd-adler32 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"simd_helpers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"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.23 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stacker 0.1.24 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",
|
||||
"strum 0.28.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"strum_macros 0.28.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"symlink 0.1.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.115 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.38.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tempfile 3.25.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.3 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.4 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.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.52.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-macros 2.7.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-native-tls 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-util 0.7.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -792,21 +788,21 @@
|
||||
"tower-layer 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tower-service 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing 0.1.44 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-appender 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-appender 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-attributes 0.1.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-core 0.1.36 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-error 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-log 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tracing-subscriber 0.3.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"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",
|
||||
"typenum 1.20.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.23 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-segmentation 1.12.0 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.13.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-width 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -815,14 +811,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.23.1 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.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.2.1 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",
|
||||
@@ -867,7 +864,6 @@
|
||||
"windows-strings 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.60.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.61.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -885,28 +881,25 @@
|
||||
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.53.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winput 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winreg 0.55.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winsafe 0.0.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winreg 0.56.0 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.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.8.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.8.48 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.8.48 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.21 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.15 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"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -931,25 +924,25 @@
|
||||
[
|
||||
"Unicode-3.0",
|
||||
[
|
||||
"icu_collections 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locale_core 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer_data 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties_data 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_provider 2.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litemap 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"potential_utf 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tinystr 0.8.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-ident 1.0.23 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"writeable 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke-derive 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom-derive 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerotrie 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec 0.11.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec-derive 0.11.2 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"icu_collections 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locale_core 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer_data 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties_data 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_provider 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litemap 0.8.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"potential_utf 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tinystr 0.8.3 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.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke 0.8.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke-derive 0.8.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom-derive 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerotrie 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec 0.11.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec-derive 0.11.3 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -970,19 +963,19 @@
|
||||
"adler32 1.2.0 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",
|
||||
"const_format 0.2.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"const_format 0.2.36 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"const_format_proc_macros 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cursor-icon 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"foldhash 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"konst 0.2.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"konst_macro_rules 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.5.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.15 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
218
docs/common-workflows/bar-widgets/systray.md
Normal file
218
docs/common-workflows/bar-widgets/systray.md
Normal 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
40
docs/common-workflows/bar.md
Normal file
40
docs/common-workflows/bar.md
Normal 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).
|
||||
@@ -172,6 +172,143 @@ consistently to all splits of that type throughout the layout. Additional values
|
||||
- Unspecified ratios default to sharing the remaining space equally
|
||||
- You only need to specify the ratios you want to customize; trailing values can be omitted
|
||||
|
||||
## Layout Options Rules
|
||||
|
||||
You can dynamically change `layout_options` based on the number of containers on a workspace
|
||||
using `layout_options_rules`. This uses the same threshold-based logic as `layout_rules`:
|
||||
when the container count is greater than or equal to a threshold, the highest matching
|
||||
threshold's options are used.
|
||||
|
||||
Rules **fully replace** the base `layout_options` when they match. If no rule matches, the
|
||||
base `layout_options` is used.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "main",
|
||||
"layout": "VerticalStack",
|
||||
"layout_options": {
|
||||
"column_ratios": [0.6],
|
||||
"row_ratios": [0.4]
|
||||
},
|
||||
"layout_options_rules": {
|
||||
"3": { "column_ratios": [0.55] },
|
||||
"5": { "column_ratios": [0.3, 0.3, 0.3], "row_ratios": [0.5] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In the example above:
|
||||
|
||||
| Container Count | Effective `layout_options` |
|
||||
|-----------------|---------------------------|
|
||||
| 1-2 | Base: `column_ratios: [0.6]`, `row_ratios: [0.4]` |
|
||||
| 3-4 | Rule "3": `column_ratios: [0.55]` (no row_ratios, no scrolling, no grid) |
|
||||
| 5+ | Rule "5": `column_ratios: [0.3, 0.3, 0.3]`, `row_ratios: [0.5]` |
|
||||
|
||||
Rules can include any field that `layout_options` supports: `column_ratios`, `row_ratios`,
|
||||
`scrolling`, and `grid`. When a rule matches, it completely replaces the base options. Fields
|
||||
not specified in the matching rule default to their standard defaults (not the base
|
||||
`layout_options` values).
|
||||
|
||||
### Example: Scrolling Layout with Dynamic Columns
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": "Scrolling",
|
||||
"layout_options": {
|
||||
"scrolling": { "columns": 2 }
|
||||
},
|
||||
"layout_options_rules": {
|
||||
"4": { "scrolling": { "columns": 3 } },
|
||||
"7": { "scrolling": { "columns": 4 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This increases the visible scrolling columns as more windows are added.
|
||||
|
||||
## Layout Defaults
|
||||
|
||||
You can define global per-layout default `layout_options` and `layout_options_rules` using
|
||||
the top-level `layout_defaults` setting. This avoids repeating the same configuration across
|
||||
every workspace that uses the same layout.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_defaults": {
|
||||
"VerticalStack": {
|
||||
"layout_options": { "column_ratios": [0.7] },
|
||||
"layout_options_rules": {
|
||||
"2": { "column_ratios": [0.7] },
|
||||
"3": { "column_ratios": [0.55] },
|
||||
"5": { "column_ratios": [0.4] }
|
||||
}
|
||||
},
|
||||
"Columns": {
|
||||
"layout_options": { "column_ratios": [0.3, 0.4] },
|
||||
"layout_options_rules": {
|
||||
"4": { "column_ratios": [0.2, 0.3, 0.3] }
|
||||
}
|
||||
},
|
||||
"HorizontalStack": {
|
||||
"layout_options": { "row_ratios": [0.6] }
|
||||
}
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "main",
|
||||
"layout": "VerticalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In this example, every workspace using `VerticalStack`, `Columns`, or `HorizontalStack`
|
||||
automatically gets the global `layout_options` and `layout_options_rules` without needing
|
||||
to specify them per-workspace. Note that `VerticalStack` only has 2 columns (main + stack),
|
||||
so only a single `column_ratios` value is meaningful, while `Columns` distributes windows
|
||||
across multiple columns where additional ratios control each column's width.
|
||||
|
||||
### Resolution Cascade
|
||||
|
||||
Global defaults act as a fallback. If a workspace defines **either** `layout_options` or
|
||||
`layout_options_rules`, it **completely replaces** all global `layout_defaults` for that
|
||||
layout. Global defaults are only used when the workspace has **neither** setting.
|
||||
|
||||
Within the effective source (workspace or global):
|
||||
1. Try threshold match from the rules (highest matching threshold wins)
|
||||
2. If a rule matches → use it (full replacement of base options)
|
||||
3. Otherwise → use the base `layout_options`
|
||||
|
||||
### Override Examples
|
||||
|
||||
| Workspace Config | Global Config | Effective Behavior |
|
||||
|------------------|---------------|--------------------|
|
||||
| No `layout_options`, no rules | `layout_defaults` has both | Uses global base + global rules |
|
||||
| Has `layout_options` only | `layout_defaults` has both | Workspace base only (all globals ignored) |
|
||||
| Has `layout_options_rules` only | `layout_defaults` has both | Workspace rules only (all globals ignored) |
|
||||
| Has both | `layout_defaults` has both | All workspace (all globals ignored) |
|
||||
|
||||
This "complete replacement" semantic means you never get a mix of workspace and global
|
||||
settings for the same layout. If you override anything at the workspace level, you take
|
||||
full control of that layout's options for that workspace.
|
||||
|
||||
## Progressive Ratio Behavior
|
||||
|
||||
Ratios are applied progressively as windows are added. For example, with `row_ratios: [0.3, 0.5]` in a VerticalStack:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.42`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}%"),
|
||||
},
|
||||
|
||||
1301
komorebi-bar/src/widgets/systray.rs
Normal file
1301
komorebi-bar/src/widgets/systray.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-layouts"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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;
|
||||
|
||||
1845
komorebi-layouts/src/arrangement_tests.rs
Normal file
1845
komorebi-layouts/src/arrangement_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -54,7 +56,7 @@ pub fn validate_ratios(ratios: &[f32]) -> [Option<f32>; MAX_RATIOS] {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// A predefined komorebi layout
|
||||
@@ -250,6 +252,21 @@ pub struct GridLayoutOptions {
|
||||
pub rows: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Per-layout default options entry for the `layout_defaults` global setting.
|
||||
/// Contains both base layout options and threshold-based layout options rules.
|
||||
pub struct LayoutDefaultEntry {
|
||||
/// Default layout options for this layout
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
/// Threshold-based layout options rules in the format of threshold => options.
|
||||
/// When container count >= threshold, the highest matching threshold's options
|
||||
/// fully replace the base `layout_options`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_options_rules: Option<HashMap<usize, LayoutOptions>>,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
@@ -420,366 +437,5 @@ impl DefaultLayout {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// 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 deserialize_ratios_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_valid_ratios() {
|
||||
let json = r#"{"column_ratios": [0.3, 0.4, 0.2]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.3));
|
||||
assert_eq!(ratios[1], Some(0.4));
|
||||
assert_eq!(ratios[2], Some(0.2));
|
||||
assert_eq!(ratios[3], None);
|
||||
assert_eq!(ratios[4], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_clamps_values_to_min() {
|
||||
// Values below MIN_RATIO should be clamped
|
||||
let json = r#"{"column_ratios": [0.05]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(MIN_RATIO)); // Clamped to 0.1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_clamps_values_to_max() {
|
||||
// Values above MAX_RATIO should be clamped
|
||||
let json = r#"{"column_ratios": [0.95]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
// 0.9 is the max, so it should be clamped
|
||||
assert!(ratios[0].unwrap() <= MAX_RATIO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_truncates_when_sum_exceeds_one() {
|
||||
// Sum of ratios should not reach 1.0
|
||||
// [0.5, 0.4] = 0.9, then 0.3 would make it 1.2, so it should be truncated
|
||||
let json = r#"{"column_ratios": [0.5, 0.4, 0.3]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.5));
|
||||
assert_eq!(ratios[1], Some(0.4));
|
||||
// Third ratio should be truncated because 0.5 + 0.4 + 0.3 >= 1.0
|
||||
assert_eq!(ratios[2], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_truncates_at_max_ratios() {
|
||||
// More than MAX_RATIOS values should be truncated
|
||||
let json = r#"{"column_ratios": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_empty_array() {
|
||||
let json = r#"{"column_ratios": []}"#;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_null() {
|
||||
let json = r#"{"column_ratios": null}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
assert!(opts.column_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_row_ratios() {
|
||||
let json = r#"{"row_ratios": [0.3, 0.5]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.row_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.3));
|
||||
assert_eq!(ratios[1], Some(0.5));
|
||||
assert_eq!(ratios[2], None);
|
||||
}
|
||||
}
|
||||
|
||||
mod serialize_ratios_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_ratios_compact() {
|
||||
let opts = layout_options_with_column_ratios(&[0.3, 0.4]);
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
// Should serialize ratios as compact array without trailing nulls in the ratios array
|
||||
assert!(json.contains("0.3") && json.contains("0.4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_none_ratios() {
|
||||
let opts = LayoutOptions {
|
||||
scrolling: None,
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: None,
|
||||
};
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
// None values should serialize as null or be omitted
|
||||
assert!(!json.contains("["));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_serialization() {
|
||||
let original = layout_options_with_column_ratios(&[0.3, 0.4, 0.2]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_row_ratios() {
|
||||
let opts = layout_options_with_row_ratios(&[0.3, 0.5]);
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
assert!(json.contains("row_ratios"));
|
||||
assert!(json.contains("0.3") && json.contains("0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_row_ratios() {
|
||||
let original = layout_options_with_row_ratios(&[0.4, 0.3]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||
assert!(original.column_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_both_ratios() {
|
||||
let original = layout_options_with_ratios(&[0.3, 0.4], &[0.5, 0.3]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||
}
|
||||
}
|
||||
|
||||
mod ratio_constants_tests {
|
||||
use super::*;
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_ratio_is_half() {
|
||||
assert_eq!(DEFAULT_RATIO, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_ratios_is_five() {
|
||||
assert_eq!(MAX_RATIOS, 5);
|
||||
}
|
||||
}
|
||||
|
||||
mod layout_options_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_layout_options_default_values() {
|
||||
let json = r#"{}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(opts.scrolling.is_none());
|
||||
assert!(opts.grid.is_none());
|
||||
assert!(opts.column_ratios.is_none());
|
||||
assert!(opts.row_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_options_with_all_fields() {
|
||||
let json = r#"{
|
||||
"scrolling": {"columns": 3},
|
||||
"grid": {"rows": 2},
|
||||
"column_ratios": [0.3, 0.4],
|
||||
"row_ratios": [0.5]
|
||||
}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(opts.scrolling.is_some());
|
||||
assert_eq!(opts.scrolling.unwrap().columns, 3);
|
||||
assert!(opts.grid.is_some());
|
||||
assert_eq!(opts.grid.unwrap().rows, 2);
|
||||
assert!(opts.column_ratios.is_some());
|
||||
assert!(opts.row_ratios.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
mod default_layout_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cycle_next_covers_all_layouts() {
|
||||
let start = DefaultLayout::BSP;
|
||||
let mut current = start;
|
||||
let mut visited = vec![current];
|
||||
|
||||
loop {
|
||||
current = current.cycle_next();
|
||||
if current == start {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
!visited.contains(¤t),
|
||||
"Cycle contains duplicate: {:?}",
|
||||
current
|
||||
);
|
||||
visited.push(current);
|
||||
}
|
||||
|
||||
// Should have visited all layouts
|
||||
assert_eq!(visited.len(), 9); // 9 layouts total
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_previous_is_inverse_of_next() {
|
||||
// Note: cycle_previous has some inconsistencies in the current implementation
|
||||
// This test documents the expected behavior for most layouts
|
||||
let layouts_with_correct_inverse = [
|
||||
DefaultLayout::Columns,
|
||||
DefaultLayout::Rows,
|
||||
DefaultLayout::VerticalStack,
|
||||
DefaultLayout::HorizontalStack,
|
||||
DefaultLayout::UltrawideVerticalStack,
|
||||
DefaultLayout::Grid,
|
||||
DefaultLayout::RightMainVerticalStack,
|
||||
];
|
||||
|
||||
for layout in layouts_with_correct_inverse {
|
||||
let next = layout.cycle_next();
|
||||
assert_eq!(
|
||||
next.cycle_previous(),
|
||||
layout,
|
||||
"cycle_previous should be inverse of cycle_next for {:?}",
|
||||
layout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_standard_layouts() {
|
||||
assert_eq!(DefaultLayout::BSP.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Columns.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Rows.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::VerticalStack.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::HorizontalStack.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Grid.leftmost_index(5), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_ultrawide() {
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(2), 1);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_right_main() {
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(2), 1);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_standard_layouts() {
|
||||
assert_eq!(DefaultLayout::BSP.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::Columns.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::Rows.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::VerticalStack.rightmost_index(5), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_right_main() {
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(5), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_ultrawide() {
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(2), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(3), 2);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(5), 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[path = "default_layout_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
954
komorebi-layouts/src/default_layout_tests.rs
Normal file
954
komorebi-layouts/src/default_layout_tests.rs
Normal file
@@ -0,0 +1,954 @@
|
||||
use super::*;
|
||||
|
||||
// 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 deserialize_ratios_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_valid_ratios() {
|
||||
let json = r#"{"column_ratios": [0.3, 0.4, 0.2]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.3));
|
||||
assert_eq!(ratios[1], Some(0.4));
|
||||
assert_eq!(ratios[2], Some(0.2));
|
||||
assert_eq!(ratios[3], None);
|
||||
assert_eq!(ratios[4], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_clamps_values_to_min() {
|
||||
// Values below MIN_RATIO should be clamped
|
||||
let json = r#"{"column_ratios": [0.05]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(MIN_RATIO)); // Clamped to 0.1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_clamps_values_to_max() {
|
||||
// Values above MAX_RATIO should be clamped
|
||||
let json = r#"{"column_ratios": [0.95]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
// 0.9 is the max, so it should be clamped
|
||||
assert!(ratios[0].unwrap() <= MAX_RATIO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_truncates_when_sum_exceeds_one() {
|
||||
// Sum of ratios should not reach 1.0
|
||||
// [0.5, 0.4] = 0.9, then 0.3 would make it 1.2, so it should be truncated
|
||||
let json = r#"{"column_ratios": [0.5, 0.4, 0.3]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.5));
|
||||
assert_eq!(ratios[1], Some(0.4));
|
||||
// Third ratio should be truncated because 0.5 + 0.4 + 0.3 >= 1.0
|
||||
assert_eq!(ratios[2], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_truncates_at_max_ratios() {
|
||||
// More than MAX_RATIOS values should be truncated
|
||||
let json = r#"{"column_ratios": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
// Only MAX_RATIOS (5) values should be stored
|
||||
for item in ratios.iter().take(MAX_RATIOS) {
|
||||
assert_eq!(*item, Some(0.1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_empty_array() {
|
||||
let json = r#"{"column_ratios": []}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
for item in ratios.iter().take(MAX_RATIOS) {
|
||||
assert_eq!(*item, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_null() {
|
||||
let json = r#"{"column_ratios": null}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
assert!(opts.column_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_row_ratios() {
|
||||
let json = r#"{"row_ratios": [0.3, 0.5]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.row_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.3));
|
||||
assert_eq!(ratios[1], Some(0.5));
|
||||
assert_eq!(ratios[2], None);
|
||||
}
|
||||
}
|
||||
|
||||
mod serialize_ratios_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_ratios_compact() {
|
||||
let opts = layout_options_with_column_ratios(&[0.3, 0.4]);
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
// Should serialize ratios as compact array without trailing nulls in the ratios array
|
||||
assert!(json.contains("0.3") && json.contains("0.4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_none_ratios() {
|
||||
let opts = LayoutOptions {
|
||||
scrolling: None,
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: None,
|
||||
};
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
// None values should serialize as null or be omitted
|
||||
assert!(!json.contains("["));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_serialization() {
|
||||
let original = layout_options_with_column_ratios(&[0.3, 0.4, 0.2]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_row_ratios() {
|
||||
let opts = layout_options_with_row_ratios(&[0.3, 0.5]);
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
assert!(json.contains("row_ratios"));
|
||||
assert!(json.contains("0.3") && json.contains("0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_row_ratios() {
|
||||
let original = layout_options_with_row_ratios(&[0.4, 0.3]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||
assert!(original.column_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_both_ratios() {
|
||||
let original = layout_options_with_ratios(&[0.3, 0.4], &[0.5, 0.3]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||
}
|
||||
}
|
||||
|
||||
mod ratio_constants_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_constants_valid_ranges() {
|
||||
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]
|
||||
fn test_default_ratio_is_half() {
|
||||
assert_eq!(DEFAULT_RATIO, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_ratios_is_five() {
|
||||
assert_eq!(MAX_RATIOS, 5);
|
||||
}
|
||||
}
|
||||
|
||||
mod layout_options_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_layout_options_default_values() {
|
||||
let json = r#"{}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(opts.scrolling.is_none());
|
||||
assert!(opts.grid.is_none());
|
||||
assert!(opts.column_ratios.is_none());
|
||||
assert!(opts.row_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_options_with_all_fields() {
|
||||
let json = r#"{
|
||||
"scrolling": {"columns": 3},
|
||||
"grid": {"rows": 2},
|
||||
"column_ratios": [0.3, 0.4],
|
||||
"row_ratios": [0.5]
|
||||
}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(opts.scrolling.is_some());
|
||||
assert_eq!(opts.scrolling.unwrap().columns, 3);
|
||||
assert!(opts.grid.is_some());
|
||||
assert_eq!(opts.grid.unwrap().rows, 2);
|
||||
assert!(opts.column_ratios.is_some());
|
||||
assert!(opts.row_ratios.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
mod default_layout_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cycle_next_covers_all_layouts() {
|
||||
let start = DefaultLayout::BSP;
|
||||
let mut current = start;
|
||||
let mut visited = vec![current];
|
||||
|
||||
loop {
|
||||
current = current.cycle_next();
|
||||
if current == start {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
!visited.contains(¤t),
|
||||
"Cycle contains duplicate: {:?}",
|
||||
current
|
||||
);
|
||||
visited.push(current);
|
||||
}
|
||||
|
||||
// Should have visited all layouts
|
||||
assert_eq!(visited.len(), 9); // 9 layouts total
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_previous_is_inverse_of_next() {
|
||||
// Note: cycle_previous has some inconsistencies in the current implementation
|
||||
// This test documents the expected behavior for most layouts
|
||||
let layouts_with_correct_inverse = [
|
||||
DefaultLayout::Columns,
|
||||
DefaultLayout::Rows,
|
||||
DefaultLayout::VerticalStack,
|
||||
DefaultLayout::HorizontalStack,
|
||||
DefaultLayout::UltrawideVerticalStack,
|
||||
DefaultLayout::Grid,
|
||||
DefaultLayout::RightMainVerticalStack,
|
||||
];
|
||||
|
||||
for layout in layouts_with_correct_inverse {
|
||||
let next = layout.cycle_next();
|
||||
assert_eq!(
|
||||
next.cycle_previous(),
|
||||
layout,
|
||||
"cycle_previous should be inverse of cycle_next for {:?}",
|
||||
layout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_standard_layouts() {
|
||||
assert_eq!(DefaultLayout::BSP.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Columns.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Rows.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::VerticalStack.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::HorizontalStack.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Grid.leftmost_index(5), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_ultrawide() {
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(2), 1);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_right_main() {
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(2), 1);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_standard_layouts() {
|
||||
assert_eq!(DefaultLayout::BSP.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::Columns.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::Rows.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::VerticalStack.rightmost_index(5), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_right_main() {
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(5), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_ultrawide() {
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(2), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(3), 2);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(5), 4);
|
||||
}
|
||||
}
|
||||
|
||||
mod layout_options_rules_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hashmap_deserialization_ratios_only() {
|
||||
// layout_options_rules entries with only ratios
|
||||
// Note: ratios must sum to < 1.0 to avoid truncation by validate_ratios
|
||||
let json = r#"{
|
||||
"2": {"column_ratios": [0.7]},
|
||||
"3": {"column_ratios": [0.55]},
|
||||
"5": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||
}"#;
|
||||
let rules: std::collections::HashMap<usize, LayoutOptions> =
|
||||
serde_json::from_str(json).unwrap();
|
||||
assert_eq!(rules.len(), 3);
|
||||
assert_eq!(rules[&2].column_ratios.unwrap()[0], Some(0.7));
|
||||
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.55));
|
||||
let r5 = rules[&5].column_ratios.unwrap();
|
||||
assert_eq!(r5[0], Some(0.3));
|
||||
assert_eq!(r5[1], Some(0.3));
|
||||
assert_eq!(r5[2], Some(0.3));
|
||||
// No scrolling/grid in these entries
|
||||
assert!(rules[&2].scrolling.is_none());
|
||||
assert!(rules[&2].grid.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashmap_deserialization_full_options() {
|
||||
// layout_options_rules entries with full options including scrolling/grid
|
||||
let json = r#"{
|
||||
"2": {"column_ratios": [0.7], "scrolling": {"columns": 3}},
|
||||
"5": {"column_ratios": [0.3, 0.3, 0.3], "grid": {"rows": 2}}
|
||||
}"#;
|
||||
let rules: std::collections::HashMap<usize, LayoutOptions> =
|
||||
serde_json::from_str(json).unwrap();
|
||||
assert_eq!(rules.len(), 2);
|
||||
assert_eq!(rules[&2].scrolling.unwrap().columns, 3);
|
||||
assert!(rules[&2].grid.is_none());
|
||||
assert!(rules[&5].scrolling.is_none());
|
||||
assert_eq!(rules[&5].grid.unwrap().rows, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rule_entry_with_all_fields() {
|
||||
let json = r#"{
|
||||
"column_ratios": [0.6, 0.3],
|
||||
"scrolling": {"columns": 4, "center_focused_column": true},
|
||||
"grid": {"rows": 2},
|
||||
"row_ratios": [0.5]
|
||||
}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
let col = opts.column_ratios.unwrap();
|
||||
assert_eq!(col[0], Some(0.6));
|
||||
assert_eq!(col[1], Some(0.3));
|
||||
let row = opts.row_ratios.unwrap();
|
||||
assert_eq!(row[0], Some(0.5));
|
||||
assert_eq!(opts.scrolling.unwrap().columns, 4);
|
||||
assert_eq!(opts.scrolling.unwrap().center_focused_column, Some(true));
|
||||
assert_eq!(opts.grid.unwrap().rows, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rule_entry_empty_object_gives_defaults() {
|
||||
let json = r#"{}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
assert!(opts.column_ratios.is_none());
|
||||
assert!(opts.row_ratios.is_none());
|
||||
assert!(opts.scrolling.is_none());
|
||||
assert!(opts.grid.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod layout_default_entry_tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_default_layout_as_hashmap_key() {
|
||||
let mut map: HashMap<DefaultLayout, &str> = HashMap::new();
|
||||
map.insert(DefaultLayout::BSP, "bsp");
|
||||
map.insert(DefaultLayout::VerticalStack, "vstack");
|
||||
map.insert(DefaultLayout::Columns, "cols");
|
||||
|
||||
assert_eq!(map.len(), 3);
|
||||
assert_eq!(map[&DefaultLayout::BSP], "bsp");
|
||||
assert_eq!(map[&DefaultLayout::VerticalStack], "vstack");
|
||||
assert_eq!(map[&DefaultLayout::Columns], "cols");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_layout_hash_consistency() {
|
||||
// Same variant inserted twice should overwrite
|
||||
let mut map: HashMap<DefaultLayout, i32> = HashMap::new();
|
||||
map.insert(DefaultLayout::Grid, 1);
|
||||
map.insert(DefaultLayout::Grid, 2);
|
||||
assert_eq!(map.len(), 1);
|
||||
assert_eq!(map[&DefaultLayout::Grid], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_default_entry_deserialize_full() {
|
||||
let json = r#"{
|
||||
"layout_options": {"column_ratios": [0.7]},
|
||||
"layout_options_rules": {
|
||||
"2": {"column_ratios": [0.7]},
|
||||
"3": {"column_ratios": [0.55]},
|
||||
"5": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||
}
|
||||
}"#;
|
||||
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||
|
||||
let base = entry.layout_options.unwrap();
|
||||
assert_eq!(base.column_ratios.unwrap()[0], Some(0.7));
|
||||
|
||||
let rules = entry.layout_options_rules.unwrap();
|
||||
assert_eq!(rules.len(), 3);
|
||||
assert_eq!(rules[&2].column_ratios.unwrap()[0], Some(0.7));
|
||||
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.55));
|
||||
let r5 = rules[&5].column_ratios.unwrap();
|
||||
assert_eq!(r5[0], Some(0.3));
|
||||
assert_eq!(r5[1], Some(0.3));
|
||||
assert_eq!(r5[2], Some(0.3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_default_entry_deserialize_only_base() {
|
||||
let json = r#"{
|
||||
"layout_options": {"column_ratios": [0.6]}
|
||||
}"#;
|
||||
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(entry.layout_options.is_some());
|
||||
assert_eq!(
|
||||
entry.layout_options.unwrap().column_ratios.unwrap()[0],
|
||||
Some(0.6)
|
||||
);
|
||||
assert!(entry.layout_options_rules.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_default_entry_deserialize_only_rules() {
|
||||
let json = r#"{
|
||||
"layout_options_rules": {
|
||||
"3": {"column_ratios": [0.4]}
|
||||
}
|
||||
}"#;
|
||||
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(entry.layout_options.is_none());
|
||||
let rules = entry.layout_options_rules.unwrap();
|
||||
assert_eq!(rules.len(), 1);
|
||||
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_default_entry_deserialize_empty() {
|
||||
let json = r#"{}"#;
|
||||
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||
assert!(entry.layout_options.is_none());
|
||||
assert!(entry.layout_options_rules.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_default_entry_roundtrip() {
|
||||
let json = r#"{
|
||||
"layout_options": {"column_ratios": [0.7]},
|
||||
"layout_options_rules": {
|
||||
"2": {"column_ratios": [0.6]},
|
||||
"5": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||
}
|
||||
}"#;
|
||||
let original: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||
let serialized = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutDefaultEntry = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
original.layout_options.unwrap().column_ratios,
|
||||
deserialized.layout_options.unwrap().column_ratios
|
||||
);
|
||||
let orig_rules = original.layout_options_rules.unwrap();
|
||||
let deser_rules = deserialized.layout_options_rules.unwrap();
|
||||
assert_eq!(orig_rules.len(), deser_rules.len());
|
||||
for (key, orig_opts) in &orig_rules {
|
||||
let deser_opts = &deser_rules[key];
|
||||
assert_eq!(orig_opts.column_ratios, deser_opts.column_ratios);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_defaults_full_config_deserialize() {
|
||||
// Simulate the top-level layout_defaults field
|
||||
let json = r#"{
|
||||
"VerticalStack": {
|
||||
"layout_options": {"column_ratios": [0.7]},
|
||||
"layout_options_rules": {
|
||||
"2": {"column_ratios": [0.7]},
|
||||
"3": {"column_ratios": [0.55]}
|
||||
}
|
||||
},
|
||||
"HorizontalStack": {
|
||||
"layout_options": {"column_ratios": [0.6]}
|
||||
},
|
||||
"Columns": {
|
||||
"layout_options_rules": {
|
||||
"4": {"column_ratios": [0.3, 0.3, 0.3]}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let defaults: HashMap<DefaultLayout, LayoutDefaultEntry> =
|
||||
serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(defaults.len(), 3);
|
||||
|
||||
// VerticalStack: has both base and rules
|
||||
let vs = &defaults[&DefaultLayout::VerticalStack];
|
||||
assert!(vs.layout_options.is_some());
|
||||
assert_eq!(vs.layout_options_rules.as_ref().unwrap().len(), 2);
|
||||
|
||||
// HorizontalStack: has only base
|
||||
let hs = &defaults[&DefaultLayout::HorizontalStack];
|
||||
assert!(hs.layout_options.is_some());
|
||||
assert!(hs.layout_options_rules.is_none());
|
||||
|
||||
// Columns: has only rules
|
||||
let cols = &defaults[&DefaultLayout::Columns];
|
||||
assert!(cols.layout_options.is_none());
|
||||
assert_eq!(cols.layout_options_rules.as_ref().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_default_entry_with_scrolling_and_grid() {
|
||||
let json = r#"{
|
||||
"layout_options": {
|
||||
"column_ratios": [0.5],
|
||||
"scrolling": {"columns": 3},
|
||||
"grid": {"rows": 2}
|
||||
},
|
||||
"layout_options_rules": {
|
||||
"4": {
|
||||
"scrolling": {"columns": 5, "center_focused_column": true}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
|
||||
|
||||
let base = entry.layout_options.unwrap();
|
||||
assert_eq!(base.scrolling.unwrap().columns, 3);
|
||||
assert_eq!(base.grid.unwrap().rows, 2);
|
||||
|
||||
let rules = entry.layout_options_rules.unwrap();
|
||||
let r4 = &rules[&4];
|
||||
assert_eq!(r4.scrolling.unwrap().columns, 5);
|
||||
assert_eq!(r4.scrolling.unwrap().center_focused_column, Some(true));
|
||||
// Rule doesn't inherit base fields - full replacement
|
||||
assert!(r4.column_ratios.is_none());
|
||||
assert!(r4.grid.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_default_entry_skip_serializing_none() {
|
||||
// When both fields are None, they should not appear in output
|
||||
let entry = LayoutDefaultEntry {
|
||||
layout_options: None,
|
||||
layout_options_rules: None,
|
||||
};
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
assert!(!json.contains("layout_options"));
|
||||
assert!(!json.contains("layout_options_rules"));
|
||||
assert_eq!(json, "{}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests for the complete-replacement cascade logic.
|
||||
///
|
||||
/// This mirrors the resolution algorithm in workspace.rs::update():
|
||||
/// - If the workspace defines EITHER layout_options OR layout_options_rules,
|
||||
/// it completely replaces the global layout_defaults for this layout.
|
||||
/// - Global defaults are only used when the workspace has NEITHER setting.
|
||||
/// - Within the effective source (workspace or global):
|
||||
/// 1. Try threshold match from rules (highest matching threshold wins)
|
||||
/// 2. If a rule matches -> use it (full replacement of base)
|
||||
/// 3. Else -> use the base layout_options
|
||||
///
|
||||
/// Since the actual cascade is in workspace.rs (which has heavy WM dependencies),
|
||||
/// we test the pure algorithm here using the same data structures.
|
||||
mod cascade_resolution_tests {
|
||||
use super::*;
|
||||
|
||||
/// Simulates the cascade resolution logic from workspace.rs::update().
|
||||
/// This is a pure function equivalent of the inline code in update().
|
||||
fn resolve_effective_options(
|
||||
container_count: usize,
|
||||
workspace_base: Option<LayoutOptions>,
|
||||
workspace_rules: &[(usize, LayoutOptions)], // sorted by threshold ascending
|
||||
global_base: Option<LayoutOptions>,
|
||||
global_rules: &[(usize, LayoutOptions)], // sorted by threshold ascending
|
||||
) -> Option<LayoutOptions> {
|
||||
let has_workspace_overrides = workspace_base.is_some() || !workspace_rules.is_empty();
|
||||
|
||||
let (effective_base, effective_rules): (Option<LayoutOptions>, &[(usize, LayoutOptions)]) =
|
||||
if has_workspace_overrides {
|
||||
(workspace_base, workspace_rules)
|
||||
} else {
|
||||
(global_base, global_rules)
|
||||
};
|
||||
|
||||
// Try threshold match from effective rules
|
||||
let mut matched = None;
|
||||
for (threshold, opts) in effective_rules {
|
||||
if container_count >= *threshold {
|
||||
matched = Some(*opts);
|
||||
}
|
||||
}
|
||||
|
||||
// If a rule matched, use it (full replacement); otherwise use effective base
|
||||
if matched.is_some() {
|
||||
matched
|
||||
} else {
|
||||
effective_base
|
||||
}
|
||||
}
|
||||
|
||||
fn opts_with_ratio(ratio: f32) -> LayoutOptions {
|
||||
layout_options_with_column_ratios(&[ratio])
|
||||
}
|
||||
|
||||
// --- No overrides ---
|
||||
|
||||
#[test]
|
||||
fn test_no_workspace_no_global_returns_none() {
|
||||
let result = resolve_effective_options(3, None, &[], None, &[]);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
// --- Base-only scenarios ---
|
||||
|
||||
#[test]
|
||||
fn test_workspace_base_only() {
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let result = resolve_effective_options(3, Some(ws_base), &[], None, &[]);
|
||||
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_base_only() {
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let result = resolve_effective_options(3, None, &[], Some(global_base), &[]);
|
||||
assert_eq!(result.unwrap().column_ratios, global_base.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_base_overrides_all_globals() {
|
||||
// Workspace has base → globals (both base and rules) are ignored entirely
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||
let result =
|
||||
resolve_effective_options(3, Some(ws_base), &[], Some(global_base), &global_rules);
|
||||
// Workspace base wins; global rules are NOT used even though they would match
|
||||
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||
}
|
||||
|
||||
// --- Rules-only scenarios ---
|
||||
|
||||
#[test]
|
||||
fn test_global_rules_match() {
|
||||
let global_rules = vec![(2, opts_with_ratio(0.6)), (4, opts_with_ratio(0.5))];
|
||||
// 3 containers: matches threshold 2, not 4
|
||||
let result = resolve_effective_options(3, None, &[], None, &global_rules);
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_rules_highest_matching_threshold_wins() {
|
||||
let global_rules = vec![(2, opts_with_ratio(0.6)), (4, opts_with_ratio(0.5))];
|
||||
// 5 containers: matches both thresholds 2 and 4; highest (4) wins
|
||||
let result = resolve_effective_options(5, None, &[], None, &global_rules);
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_rules_no_match_falls_through_to_none() {
|
||||
let global_rules = vec![(5, opts_with_ratio(0.5))];
|
||||
// 3 containers: doesn't match threshold 5
|
||||
let result = resolve_effective_options(3, None, &[], None, &global_rules);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_rules_no_match_falls_through_to_global_base() {
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let global_rules = vec![(5, opts_with_ratio(0.5))];
|
||||
// 3 containers: doesn't match threshold 5, falls back to global base
|
||||
let result = resolve_effective_options(3, None, &[], Some(global_base), &global_rules);
|
||||
assert_eq!(result.unwrap().column_ratios, global_base.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_rules_override_global_rules() {
|
||||
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||
let global_rules = vec![(2, opts_with_ratio(0.6))];
|
||||
// Workspace has rules → global rules are ignored entirely
|
||||
let result = resolve_effective_options(3, None, &ws_rules, None, &global_rules);
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||
}
|
||||
|
||||
// --- Complete replacement: workspace having EITHER setting disables ALL globals ---
|
||||
|
||||
#[test]
|
||||
fn test_workspace_rules_disable_global_base() {
|
||||
// Workspace has rules but no base. Global has base.
|
||||
// Since workspace has a setting, globals are completely replaced.
|
||||
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
// Rule matches → use it. Global base is NOT available as fallback.
|
||||
let result = resolve_effective_options(3, None, &ws_rules, Some(global_base), &[]);
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_rules_no_match_does_not_fall_to_global_base() {
|
||||
// Workspace has rules (but they don't match). Global has base.
|
||||
// Since workspace has a setting, globals are completely replaced → returns None.
|
||||
let ws_rules = vec![(5, opts_with_ratio(0.8))];
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let result = resolve_effective_options(3, None, &ws_rules, Some(global_base), &[]);
|
||||
// No workspace base, no rule match, globals ignored → None
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_base_disables_global_rules() {
|
||||
// Workspace has base but no rules. Global has rules.
|
||||
// Since workspace has a setting, globals are completely replaced.
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||
// No workspace rules → no rule match → use workspace base. Global rules ignored.
|
||||
let result = resolve_effective_options(3, Some(ws_base), &[], None, &global_rules);
|
||||
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_base_disables_global_rules_and_base() {
|
||||
// Workspace has base. Global has both rules and base.
|
||||
// Since workspace has a setting, all globals are completely replaced.
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||
let result =
|
||||
resolve_effective_options(3, Some(ws_base), &[], Some(global_base), &global_rules);
|
||||
// Only workspace base is used; global rules and base are both ignored
|
||||
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_rules_disable_global_rules_and_base() {
|
||||
// Workspace has rules. Global has both rules and base.
|
||||
// Since workspace has a setting, all globals are completely replaced.
|
||||
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||
let result =
|
||||
resolve_effective_options(3, None, &ws_rules, Some(global_base), &global_rules);
|
||||
// Workspace rule matches → 0.8. Global base and rules both ignored.
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||
}
|
||||
|
||||
// --- Full replacement semantics (rule match replaces base) ---
|
||||
|
||||
#[test]
|
||||
fn test_rule_match_is_full_replacement_not_merge() {
|
||||
// When a rule matches, its options FULLY REPLACE the base.
|
||||
// Fields not specified in the rule default to their standard defaults.
|
||||
let ws_base = layout_options_with_ratios(&[0.7], &[0.4]);
|
||||
let rule_opts = layout_options_with_column_ratios(&[0.5]);
|
||||
// rule_opts has column_ratios but no row_ratios
|
||||
let ws_rules = vec![(2, rule_opts)];
|
||||
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
|
||||
let effective = result.unwrap();
|
||||
// Column ratios come from the rule
|
||||
assert_eq!(effective.column_ratios.unwrap()[0], Some(0.5));
|
||||
// Row ratios are NOT inherited from ws_base - they're None (full replacement)
|
||||
assert!(effective.row_ratios.is_none());
|
||||
}
|
||||
|
||||
// --- Edge cases ---
|
||||
|
||||
#[test]
|
||||
fn test_exact_threshold_match() {
|
||||
let rules = vec![(3, opts_with_ratio(0.6))];
|
||||
let result = resolve_effective_options(3, None, &rules, None, &[]);
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_count_one_below_threshold() {
|
||||
let rules = vec![(3, opts_with_ratio(0.6))];
|
||||
let result = resolve_effective_options(2, None, &rules, None, &[]);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_containers() {
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let rules = vec![(1, opts_with_ratio(0.5))];
|
||||
let result = resolve_effective_options(0, Some(ws_base), &rules, None, &[]);
|
||||
// 0 containers doesn't match threshold 1 → falls back to workspace base
|
||||
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_many_thresholds_correct_match() {
|
||||
let rules = vec![
|
||||
(1, opts_with_ratio(0.8)),
|
||||
(3, opts_with_ratio(0.6)),
|
||||
(5, opts_with_ratio(0.4)),
|
||||
(8, opts_with_ratio(0.3)),
|
||||
];
|
||||
// 6 containers: matches 1, 3, 5 but not 8. Highest match is 5.
|
||||
let result = resolve_effective_options(6, None, &rules, None, &[]);
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_rules_disable_global_rules_even_if_ws_rules_dont_match() {
|
||||
// Key behavior: if workspace has ANY setting, globals are entirely ignored.
|
||||
// Even if workspace rules don't match, we don't fall back to global rules.
|
||||
let ws_rules = vec![(10, opts_with_ratio(0.8))]; // threshold too high
|
||||
let global_rules = vec![(2, opts_with_ratio(0.5))]; // would match
|
||||
let result = resolve_effective_options(3, None, &ws_rules, None, &global_rules);
|
||||
// Workspace has rules → all globals ignored. WS rules don't match → None.
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_four_sources_present_rules_match() {
|
||||
// All four sources present: workspace base, workspace rules, global base, global rules
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let ws_rules = vec![(2, opts_with_ratio(0.8))];
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let global_rules = vec![(2, opts_with_ratio(0.5))];
|
||||
let result = resolve_effective_options(
|
||||
3,
|
||||
Some(ws_base),
|
||||
&ws_rules,
|
||||
Some(global_base),
|
||||
&global_rules,
|
||||
);
|
||||
// Workspace has settings → uses workspace only. Rule matches → 0.8
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_four_sources_present_rules_no_match() {
|
||||
// All four sources present, but workspace rules don't match
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let ws_rules = vec![(10, opts_with_ratio(0.8))]; // threshold too high
|
||||
let global_base = opts_with_ratio(0.6);
|
||||
let global_rules = vec![(10, opts_with_ratio(0.5))]; // also too high
|
||||
let result = resolve_effective_options(
|
||||
3,
|
||||
Some(ws_base),
|
||||
&ws_rules,
|
||||
Some(global_base),
|
||||
&global_rules,
|
||||
);
|
||||
// Workspace has settings → uses workspace only. No rule match → workspace base 0.7
|
||||
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||
}
|
||||
|
||||
// --- Workspace with both base and rules ---
|
||||
|
||||
#[test]
|
||||
fn test_workspace_both_rule_matches() {
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let ws_rules = vec![(2, opts_with_ratio(0.5))];
|
||||
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
|
||||
// Rule matches → use rule (full replacement), not ws_base
|
||||
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_both_rule_no_match() {
|
||||
let ws_base = opts_with_ratio(0.7);
|
||||
let ws_rules = vec![(10, opts_with_ratio(0.5))];
|
||||
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
|
||||
// Rule doesn't match → fall back to ws_base
|
||||
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
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",
|
||||
@@ -15,7 +15,7 @@ serde = { workspace = true }
|
||||
serde_variant = "0.1"
|
||||
strum = { workspace = true }
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
|
||||
flavours = { git = "https://github.com/LGUG2Z/flavours", rev = "24518c129918fe3260aa559eded7657e50752cb1" }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
@@ -51,8 +51,8 @@ windows-numerics = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.55"
|
||||
serde_with = { version = "3.12", features = ["schemars_1"] }
|
||||
winreg = "0.56"
|
||||
serde_with = { version = "3.19", features = ["schemars_1"] }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
@@ -86,6 +86,7 @@ impl AnimationEngine {
|
||||
{
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
|
||||
render_dispatcher.cleanup_on_cancel();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
363
komorebi/src/animation/ghost.rs
Normal file
363
komorebi/src/animation/ghost.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
use color_eyre::eyre;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_channel::bounded;
|
||||
use crossbeam_channel::unbounded;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Dwm::DWM_THUMBNAIL_PROPERTIES;
|
||||
use windows::Win32::Graphics::Dwm::DWM_TNP_OPACITY;
|
||||
use windows::Win32::Graphics::Dwm::DWM_TNP_RECTDESTINATION;
|
||||
use windows::Win32::Graphics::Dwm::DWM_TNP_SOURCECLIENTAREAONLY;
|
||||
use windows::Win32::Graphics::Dwm::DWM_TNP_VISIBLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DestroyWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_NOREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_NOZORDER;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
|
||||
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
use crate::WindowsApi;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
|
||||
const GHOST_CLASS_NAME: &[u16] = &[
|
||||
b'k' as u16,
|
||||
b'o' as u16,
|
||||
b'm' as u16,
|
||||
b'o' as u16,
|
||||
b'r' as u16,
|
||||
b'e' as u16,
|
||||
b'b' as u16,
|
||||
b'i' as u16,
|
||||
b'-' as u16,
|
||||
b'g' as u16,
|
||||
b'h' as u16,
|
||||
b'o' as u16,
|
||||
b's' as u16,
|
||||
b't' as u16,
|
||||
0,
|
||||
];
|
||||
|
||||
enum GhostCmd {
|
||||
Create {
|
||||
src_hwnd: isize,
|
||||
start_rect: Rect,
|
||||
z_above: Option<isize>,
|
||||
reply: Sender<eyre::Result<(isize, isize)>>,
|
||||
},
|
||||
UpdateRect {
|
||||
host_hwnd: isize,
|
||||
hthumb: isize,
|
||||
rect: Rect,
|
||||
},
|
||||
Destroy {
|
||||
host_hwnd: isize,
|
||||
hthumb: isize,
|
||||
},
|
||||
}
|
||||
|
||||
struct GhostOwner {
|
||||
cmd_tx: Sender<GhostCmd>,
|
||||
}
|
||||
|
||||
static GHOST_OWNER: OnceLock<GhostOwner> = OnceLock::new();
|
||||
|
||||
fn ghost_owner() -> &'static GhostOwner {
|
||||
GHOST_OWNER.get_or_init(|| {
|
||||
let (tx, rx) = unbounded::<GhostCmd>();
|
||||
std::thread::Builder::new()
|
||||
.name("komorebi-ghost-owner".into())
|
||||
.spawn(move || run_owner_loop(rx))
|
||||
.expect("failed to spawn ghost owner thread");
|
||||
GhostOwner { cmd_tx: tx }
|
||||
})
|
||||
}
|
||||
|
||||
/// Eagerly initialise the ghost owner thread so the first movement animation
|
||||
/// doesn't pay the spawn + class-registration cost. Idempotent. No-op for
|
||||
/// users who never enable ghost movement only if it isn't called; calling
|
||||
/// from a code path that's gated on `GHOST_MOVEMENT_ENABLED` keeps the lazy
|
||||
/// guarantee.
|
||||
pub fn prewarm() {
|
||||
let _ = ghost_owner();
|
||||
}
|
||||
|
||||
extern "system" fn ghost_wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
|
||||
}
|
||||
|
||||
fn register_ghost_class() -> eyre::Result<()> {
|
||||
let h_module = WindowsApi::module_handle_w()?;
|
||||
let class_name = PCWSTR(GHOST_CLASS_NAME.as_ptr());
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
lpfnWndProc: Some(ghost_wnd_proc),
|
||||
..Default::default()
|
||||
};
|
||||
// RegisterClassW returns 0 on failure with ERROR_CLASS_ALREADY_EXISTS as a
|
||||
// benign error if the class is already registered. We tolerate that.
|
||||
let _ = WindowsApi::register_class_w(&window_class);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_owner_loop(cmd_rx: crossbeam_channel::Receiver<GhostCmd>) {
|
||||
if let Err(error) = register_ghost_class() {
|
||||
tracing::error!("ghost owner: failed to register class: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
// Drain any pending Win32 messages (DWM/system messages destined for our hosts).
|
||||
unsafe {
|
||||
let mut msg = MSG::default();
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
match cmd_rx.recv_timeout(Duration::from_millis(8)) {
|
||||
Ok(cmd) => handle_cmd(cmd),
|
||||
Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
|
||||
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_cmd(cmd: GhostCmd) {
|
||||
match cmd {
|
||||
GhostCmd::Create {
|
||||
src_hwnd,
|
||||
start_rect,
|
||||
z_above,
|
||||
reply,
|
||||
} => {
|
||||
let result = create_ghost(src_hwnd, start_rect, z_above);
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
GhostCmd::UpdateRect {
|
||||
host_hwnd,
|
||||
hthumb,
|
||||
rect,
|
||||
} => {
|
||||
if let Err(error) = update_ghost(host_hwnd, hthumb, rect) {
|
||||
tracing::trace!("ghost owner: update failed: {error}");
|
||||
}
|
||||
}
|
||||
GhostCmd::Destroy { host_hwnd, hthumb } => {
|
||||
destroy_ghost(host_hwnd, hthumb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn instance_handle() -> eyre::Result<isize> {
|
||||
let h_module = WindowsApi::module_handle_w()?;
|
||||
Ok(h_module.0 as isize)
|
||||
}
|
||||
|
||||
fn create_ghost(
|
||||
src_hwnd: isize,
|
||||
start_rect: Rect,
|
||||
z_above: Option<isize>,
|
||||
) -> eyre::Result<(isize, isize)> {
|
||||
let class_name = PCWSTR(GHOST_CLASS_NAME.as_ptr());
|
||||
let host_hwnd = WindowsApi::create_ghost_host_window(class_name, instance_handle()?)?;
|
||||
|
||||
// Position the host at start_rect (Rect uses left/top + width/height).
|
||||
let z_after = match z_above {
|
||||
Some(hwnd) => HWND(windows_api::as_ptr!(hwnd)),
|
||||
None => HWND_TOP,
|
||||
};
|
||||
let flags = SWP_NOACTIVATE | SWP_NOREDRAW | SWP_SHOWWINDOW;
|
||||
unsafe {
|
||||
let _ = SetWindowPos(
|
||||
HWND(windows_api::as_ptr!(host_hwnd)),
|
||||
Option::from(z_after),
|
||||
start_rect.left,
|
||||
start_rect.top,
|
||||
start_rect.right,
|
||||
start_rect.bottom,
|
||||
flags,
|
||||
);
|
||||
}
|
||||
|
||||
let hthumb = match WindowsApi::dwm_register_thumbnail(host_hwnd, src_hwnd) {
|
||||
Ok(h) => h,
|
||||
Err(error) => {
|
||||
unsafe {
|
||||
let _ = DestroyWindow(HWND(windows_api::as_ptr!(host_hwnd)));
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
let props = thumbnail_properties(start_rect.right, start_rect.bottom);
|
||||
if let Err(error) = WindowsApi::dwm_update_thumbnail_properties(hthumb, &props) {
|
||||
let _ = WindowsApi::dwm_unregister_thumbnail(hthumb);
|
||||
unsafe {
|
||||
let _ = DestroyWindow(HWND(windows_api::as_ptr!(host_hwnd)));
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Make the host visible. Layered/transparent ext styles ensure no input.
|
||||
unsafe {
|
||||
let _ = ShowWindow(
|
||||
HWND(windows_api::as_ptr!(host_hwnd)),
|
||||
SHOW_WINDOW_CMD(8), // SW_SHOWNA
|
||||
);
|
||||
}
|
||||
|
||||
Ok((host_hwnd, hthumb))
|
||||
}
|
||||
|
||||
fn update_ghost(host_hwnd: isize, hthumb: isize, rect: Rect) -> eyre::Result<()> {
|
||||
let flags: SET_WINDOW_POS_FLAGS = SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOREDRAW;
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
HWND(windows_api::as_ptr!(host_hwnd)),
|
||||
None,
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom,
|
||||
flags,
|
||||
)?;
|
||||
}
|
||||
|
||||
let props = thumbnail_properties(rect.right, rect.bottom);
|
||||
WindowsApi::dwm_update_thumbnail_properties(hthumb, &props)
|
||||
}
|
||||
|
||||
fn destroy_ghost(host_hwnd: isize, hthumb: isize) {
|
||||
let _ = WindowsApi::dwm_unregister_thumbnail(hthumb);
|
||||
unsafe {
|
||||
let _ = DestroyWindow(HWND(windows_api::as_ptr!(host_hwnd)));
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbnail_properties(width: i32, height: i32) -> DWM_THUMBNAIL_PROPERTIES {
|
||||
DWM_THUMBNAIL_PROPERTIES {
|
||||
dwFlags: DWM_TNP_VISIBLE
|
||||
| DWM_TNP_RECTDESTINATION
|
||||
| DWM_TNP_OPACITY
|
||||
| DWM_TNP_SOURCECLIENTAREAONLY,
|
||||
rcDestination: RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: width,
|
||||
bottom: height,
|
||||
},
|
||||
rcSource: RECT::default(),
|
||||
opacity: 255,
|
||||
fVisible: true.into(),
|
||||
fSourceClientAreaOnly: false.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A live DWM-thumbnail "ghost" of a source window, used during movement
|
||||
/// animations. While a ghost is active, the source window is typically cloaked
|
||||
/// by the caller. The ghost is automatically disposed on drop, but callers
|
||||
/// should prefer explicit `dispose()` to surface errors.
|
||||
pub struct GhostWindow {
|
||||
host_hwnd: isize,
|
||||
hthumb: isize,
|
||||
disposed: bool,
|
||||
}
|
||||
|
||||
impl GhostWindow {
|
||||
pub fn create(src_hwnd: isize, start_rect: Rect, z_above: Option<isize>) -> eyre::Result<Self> {
|
||||
let (reply_tx, reply_rx) = bounded::<eyre::Result<(isize, isize)>>(1);
|
||||
ghost_owner()
|
||||
.cmd_tx
|
||||
.send(GhostCmd::Create {
|
||||
src_hwnd,
|
||||
start_rect,
|
||||
z_above,
|
||||
reply: reply_tx,
|
||||
})
|
||||
.map_err(|e| eyre::eyre!("ghost owner channel send failed: {e}"))?;
|
||||
let (host_hwnd, hthumb) = reply_rx.recv()??;
|
||||
Ok(Self {
|
||||
host_hwnd,
|
||||
hthumb,
|
||||
disposed: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn host_hwnd(&self) -> isize {
|
||||
self.host_hwnd
|
||||
}
|
||||
|
||||
pub fn update_rect(&self, rect: Rect) -> eyre::Result<()> {
|
||||
ghost_owner()
|
||||
.cmd_tx
|
||||
.send(GhostCmd::UpdateRect {
|
||||
host_hwnd: self.host_hwnd,
|
||||
hthumb: self.hthumb,
|
||||
rect,
|
||||
})
|
||||
.map_err(|e| eyre::eyre!("ghost owner channel send failed: {e}"))
|
||||
}
|
||||
|
||||
/// Apply an opacity change directly via `DwmUpdateThumbnailProperties` on
|
||||
/// the calling thread. Unlike rect updates (which call `SetWindowPos` and
|
||||
/// therefore need the owner thread), opacity-only updates don't have
|
||||
/// thread affinity, and going through the channel introduces a race where
|
||||
/// the next `DwmFlush()` on the caller's thread can fire before the owner
|
||||
/// has processed the SetOpacity command — which collapses what should be
|
||||
/// a multi-frame fade into a single visible step.
|
||||
pub fn set_opacity(&self, opacity: u8) -> eyre::Result<()> {
|
||||
let props = DWM_THUMBNAIL_PROPERTIES {
|
||||
dwFlags: DWM_TNP_OPACITY | DWM_TNP_VISIBLE,
|
||||
rcDestination: RECT::default(),
|
||||
rcSource: RECT::default(),
|
||||
opacity,
|
||||
fVisible: true.into(),
|
||||
fSourceClientAreaOnly: false.into(),
|
||||
};
|
||||
WindowsApi::dwm_update_thumbnail_properties(self.hthumb, &props)
|
||||
}
|
||||
|
||||
pub fn dispose(mut self) -> eyre::Result<()> {
|
||||
self.dispose_inner()
|
||||
}
|
||||
|
||||
fn dispose_inner(&mut self) -> eyre::Result<()> {
|
||||
if self.disposed {
|
||||
return Ok(());
|
||||
}
|
||||
self.disposed = true;
|
||||
ghost_owner()
|
||||
.cmd_tx
|
||||
.send(GhostCmd::Destroy {
|
||||
host_hwnd: self.host_hwnd,
|
||||
hthumb: self.hthumb,
|
||||
})
|
||||
.map_err(|e| eyre::eyre!("ghost owner channel send failed: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GhostWindow {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.dispose_inner();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use parking_lot::Mutex;
|
||||
pub use engine::AnimationEngine;
|
||||
pub mod animation_manager;
|
||||
pub mod engine;
|
||||
pub mod ghost;
|
||||
pub mod lerp;
|
||||
pub mod prefix;
|
||||
pub mod render_dispatcher;
|
||||
@@ -59,6 +60,7 @@ pub const DEFAULT_ANIMATION_ENABLED: bool = false;
|
||||
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
|
||||
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
|
||||
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
|
||||
pub const DEFAULT_GHOST_MOVEMENT: bool = true;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
@@ -78,3 +80,4 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);
|
||||
pub static GHOST_MOVEMENT_ENABLED: AtomicBool = AtomicBool::new(DEFAULT_GHOST_MOVEMENT);
|
||||
|
||||
@@ -5,4 +5,10 @@ pub trait RenderDispatcher {
|
||||
fn pre_render(&self) -> eyre::Result<()>;
|
||||
fn render(&self, delta: f64) -> eyre::Result<()>;
|
||||
fn post_render(&self) -> eyre::Result<()>;
|
||||
|
||||
/// Called by the animation engine when an in-flight animation is cancelled
|
||||
/// before it could complete. Implementors should use this to release any
|
||||
/// resources allocated in `pre_render` and bring the underlying window
|
||||
/// back to a consistent visible state. Default: no-op.
|
||||
fn cleanup_on_cancel(&self) {}
|
||||
}
|
||||
|
||||
@@ -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,21 @@ 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;
|
||||
|
||||
/// Custom WM_USER message used to drive the border in lockstep with an active
|
||||
/// movement animation. lparam carries a `Box<Rect>` ownership transfer that the
|
||||
/// receiving WndProc reclaims and applies as the new tracked rect.
|
||||
pub const WM_ANIMATE_RECT: u32 = WM_USER + 2;
|
||||
|
||||
pub struct RenderFactory(ID2D1Factory);
|
||||
unsafe impl Sync for RenderFactory {}
|
||||
unsafe impl Send for RenderFactory {}
|
||||
@@ -100,6 +111,98 @@ static BRUSH_PROPERTIES: LazyLock<D2D1_BRUSH_PROPERTIES> =
|
||||
transform: Matrix3x2::identity(),
|
||||
});
|
||||
|
||||
/// Apply a new tracked rect to the border on its own message-loop thread.
|
||||
/// Updates `window_rect`, calls `set_position`, and re-renders if size/position
|
||||
/// changed. Used by both `EVENT_OBJECT_LOCATIONCHANGE` (real window movements)
|
||||
/// and `WM_ANIMATE_RECT` (animation-driven movements while the source is cloaked).
|
||||
///
|
||||
/// SAFETY: caller must ensure `border_pointer` is non-null, points to a live
|
||||
/// `Border`, and that we are running on the border's WndProc thread.
|
||||
unsafe fn apply_tracked_rect(border_pointer: *mut Border, rect: Rect) {
|
||||
unsafe {
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
let old_rect = (*border_pointer).window_rect;
|
||||
(*border_pointer).window_rect = rect;
|
||||
|
||||
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if (rect.is_same_size_as(&old_rect) && rect.has_same_position_as(&old_rect))
|
||||
|| (*border_pointer).render_target.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// double-check destruction flag before rendering
|
||||
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
|
||||
let render_target = match (*border_pointer).render_target.as_ref() {
|
||||
Some(rt) => rt,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
let Some(brush) = (*border_pointer).brushes.get(&window_kind) else {
|
||||
return;
|
||||
};
|
||||
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
|
||||
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
let hwnd = hwnd.0 as isize;
|
||||
@@ -318,20 +421,54 @@ 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),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Drive the border to follow `rect` during a movement animation. Hands
|
||||
/// ownership of a boxed `Rect` to the border's message-loop thread via
|
||||
/// `WM_ANIMATE_RECT`, which mirrors the redraw path normally driven by
|
||||
/// `EVENT_OBJECT_LOCATIONCHANGE` on the real source window.
|
||||
pub fn animate_to(&self, rect: Rect) {
|
||||
let boxed = Box::new(rect);
|
||||
let ptr = Box::into_raw(boxed);
|
||||
let posted = unsafe {
|
||||
PostMessageW(
|
||||
Option::from(self.hwnd()),
|
||||
WM_ANIMATE_RECT,
|
||||
WPARAM(0),
|
||||
LPARAM(ptr as isize),
|
||||
)
|
||||
};
|
||||
if posted.is_err() {
|
||||
// Reclaim the box on failure to avoid leaking.
|
||||
unsafe {
|
||||
drop(Box::from_raw(ptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
||||
let mut rect = *rect;
|
||||
rect.add_margin(self.width);
|
||||
@@ -402,81 +539,24 @@ impl Border {
|
||||
}
|
||||
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
|
||||
let old_rect = (*border_pointer).window_rect;
|
||||
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
|
||||
apply_tracked_rect(border_pointer, rect);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_ANIMATE_RECT => {
|
||||
// lparam carries an owned Box<Rect> from the animation thread.
|
||||
let rect_box = Box::from_raw(lparam.0 as *mut Rect);
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
(*border_pointer).window_rect = rect;
|
||||
|
||||
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
|
||||
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
|
||||
{
|
||||
// double-check destruction flag before rendering
|
||||
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
apply_tracked_rect(border_pointer, *rect_box);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_PAINT => {
|
||||
@@ -576,6 +656,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() {
|
||||
|
||||
@@ -113,6 +113,20 @@ pub fn window_border(hwnd: isize) -> Option<BorderInfo> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Drive the border that tracks `source_hwnd` to follow `rect`. No-op when no
|
||||
/// border is registered for the source window. Used by movement animations to
|
||||
/// keep the border visually in sync while the source window is cloaked.
|
||||
pub fn animate_to(source_hwnd: isize, rect: crate::core::Rect) {
|
||||
let border_id = match WINDOWS_BORDERS.lock().get(&source_hwnd).cloned() {
|
||||
Some(id) => id,
|
||||
None => return,
|
||||
};
|
||||
let state = BORDER_STATE.lock();
|
||||
if let Some(border) = state.get(&border_id) {
|
||||
border.animate_to(rect);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
if event_tx().try_send(Notification::Update(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
@@ -451,8 +465,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 +633,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 +719,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();
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -31,6 +32,7 @@ pub use komorebi_layouts::DefaultLayout;
|
||||
pub use komorebi_layouts::Direction;
|
||||
pub use komorebi_layouts::GridLayoutOptions;
|
||||
pub use komorebi_layouts::Layout;
|
||||
pub use komorebi_layouts::LayoutDefaultEntry;
|
||||
pub use komorebi_layouts::LayoutOptions;
|
||||
pub use komorebi_layouts::MAX_RATIO;
|
||||
pub use komorebi_layouts::MAX_RATIOS;
|
||||
@@ -256,6 +258,8 @@ pub enum SocketMessage {
|
||||
StaticConfigSchema,
|
||||
GenerateStaticConfig,
|
||||
DebugWindow(isize),
|
||||
// low level commands
|
||||
ApplyState(State),
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
|
||||
@@ -238,6 +238,9 @@ lazy_static! {
|
||||
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
|
||||
|
||||
static ref CURRENT_VIRTUAL_DESKTOP: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
pub static ref LAYOUT_DEFAULTS: Arc<Mutex<HashMap<DefaultLayout, LayoutDefaultEntry>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
@@ -322,7 +325,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)]
|
||||
|
||||
@@ -307,12 +307,10 @@ impl Monitor {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
workspace.add_container_to_front(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if workspace.containers().len() == 1 {
|
||||
workspace.insert_container_at_idx(0, container);
|
||||
} else {
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack
|
||||
if workspace.containers().len() == 1 =>
|
||||
{
|
||||
workspace.insert_container_at_idx(0, container);
|
||||
}
|
||||
_ => {
|
||||
workspace.add_container_to_back(container);
|
||||
@@ -332,12 +330,10 @@ impl Monitor {
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if workspace.containers().len() == 1 {
|
||||
workspace.add_container_to_back(container);
|
||||
} else {
|
||||
workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
| DefaultLayout::UltrawideVerticalStack
|
||||
if workspace.containers().len() == 1 =>
|
||||
{
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
_ => {
|
||||
workspace.insert_container_at_idx(target_index, container);
|
||||
|
||||
@@ -2296,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(_, _) => {}
|
||||
|
||||
@@ -28,12 +28,10 @@ pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
|
||||
Action::Press => ignore_movement = true,
|
||||
Action::Release => ignore_movement = false,
|
||||
},
|
||||
Event::MouseMoveRelative { .. } => {
|
||||
if !ignore_movement {
|
||||
match wm.lock().raise_window_at_cursor_pos() {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
Event::MouseMoveRelative { .. } if !ignore_movement => {
|
||||
match wm.lock().raise_window_at_cursor_pos() {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -253,6 +253,9 @@ impl From<&WindowManager> for State {
|
||||
layout: workspace.layout.clone(),
|
||||
layout_options: workspace.layout_options,
|
||||
layout_rules: workspace.layout_rules.clone(),
|
||||
layout_options_rules: workspace.layout_options_rules.clone(),
|
||||
layout_defaults_cache: workspace.layout_defaults_cache.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,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::FloatingLayerBehaviour;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::LAYOUT_DEFAULTS;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
@@ -38,6 +39,8 @@ use crate::animation::ANIMATION_FPS;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::animation::DEFAULT_ANIMATION_FPS;
|
||||
use crate::animation::DEFAULT_GHOST_MOVEMENT;
|
||||
use crate::animation::GHOST_MOVEMENT_ENABLED;
|
||||
use crate::animation::PerAnimationPrefixConfig;
|
||||
use crate::asc::ApplicationSpecificConfiguration;
|
||||
use crate::asc::AscApplicationRulesOrSchema;
|
||||
@@ -53,6 +56,7 @@ use crate::core::DefaultLayout;
|
||||
use crate::core::FocusFollowsMouseImplementation;
|
||||
use crate::core::HidingBehaviour;
|
||||
use crate::core::Layout;
|
||||
use crate::core::LayoutDefaultEntry;
|
||||
use crate::core::LayoutOptions;
|
||||
use crate::core::MoveBehaviour;
|
||||
use crate::core::OperationBehaviour;
|
||||
@@ -215,6 +219,12 @@ pub struct WorkspaceConfig {
|
||||
/// Layout-specific options
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
/// Threshold-based layout options rules in the format of threshold => options.
|
||||
/// When container count >= threshold, the highest matching threshold's options
|
||||
/// fully replace the base `layout_options`.
|
||||
/// This follows the same threshold logic as `layout_rules`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_options_rules: Option<HashMap<usize, LayoutOptions>>,
|
||||
/// END OF LIFE FEATURE: Custom Layout
|
||||
#[deprecated(note = "End of life feature")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -223,6 +233,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 +300,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);
|
||||
@@ -332,6 +352,11 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
);
|
||||
value.layout_options
|
||||
},
|
||||
layout_options_rules: if value.layout_options_rules.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.layout_options_rules.iter().copied().collect())
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
custom_layout: value
|
||||
.workspace_config
|
||||
@@ -353,6 +378,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 +477,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.42`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[deprecated(note = "No longer required")]
|
||||
@@ -571,6 +597,11 @@ pub struct StaticConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_CONTAINER_PADDING)))]
|
||||
pub default_container_padding: Option<i32>,
|
||||
/// Per-layout default options and rules, keyed by layout name.
|
||||
/// Applied as fallback when a workspace does not define its own layout_options or layout_options_rules.
|
||||
/// If a workspace defines either setting, all global defaults for that layout are completely replaced.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_defaults: Option<HashMap<DefaultLayout, LayoutDefaultEntry>>,
|
||||
/// Monitor and workspace configurations
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitors: Option<Vec<MonitorConfig>>,
|
||||
@@ -666,6 +697,11 @@ pub struct AnimationsConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = ANIMATION_FPS)))]
|
||||
pub fps: Option<u64>,
|
||||
/// Render movement animations on a GPU-composited ghost surface (recommended).
|
||||
/// When false, falls back to the legacy per-frame MoveWindow path.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub ghost_movement: Option<bool>,
|
||||
}
|
||||
|
||||
pub use komorebi_themes::KomorebiTheme;
|
||||
@@ -880,6 +916,14 @@ impl From<&WindowManager> for StaticConfig {
|
||||
default_container_padding: Option::from(
|
||||
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
layout_defaults: {
|
||||
let guard = LAYOUT_DEFAULTS.lock();
|
||||
if guard.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(guard.clone())
|
||||
}
|
||||
},
|
||||
monitors: Option::from(monitors),
|
||||
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
|
||||
global_work_area_offset: value.work_area_offset,
|
||||
@@ -985,6 +1029,17 @@ impl StaticConfig {
|
||||
animations.fps.unwrap_or(DEFAULT_ANIMATION_FPS),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
let ghost_movement_enabled =
|
||||
animations.ghost_movement.unwrap_or(DEFAULT_GHOST_MOVEMENT);
|
||||
GHOST_MOVEMENT_ENABLED.store(ghost_movement_enabled, Ordering::SeqCst);
|
||||
if ghost_movement_enabled {
|
||||
// Spawn the ghost owner thread now so the first animation
|
||||
// doesn't pay the spawn + wndclass-registration cost. Lazy
|
||||
// guarantee preserved: users who turn ghost_movement off
|
||||
// never trigger this path, so the thread is never created.
|
||||
crate::animation::ghost::prewarm();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = self.default_container_padding {
|
||||
@@ -995,6 +1050,12 @@ impl StaticConfig {
|
||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(defaults) = &self.layout_defaults {
|
||||
*LAYOUT_DEFAULTS.lock() = defaults.clone();
|
||||
} else {
|
||||
LAYOUT_DEFAULTS.lock().clear();
|
||||
}
|
||||
|
||||
if let Some(border_width) = self.border_width {
|
||||
border_manager::BORDER_WIDTH.store(border_width, Ordering::SeqCst);
|
||||
}
|
||||
@@ -1403,7 +1464,7 @@ impl StaticConfig {
|
||||
workspace_config.layout = Some(DefaultLayout::Columns);
|
||||
}
|
||||
|
||||
ws.load_static_config(workspace_config)?;
|
||||
ws.load_static_config(workspace_config, value.layout_defaults.as_ref())?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1486,7 +1547,10 @@ impl StaticConfig {
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
ws.load_static_config(
|
||||
workspace_config,
|
||||
value.layout_defaults.as_ref(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1568,7 +1632,7 @@ impl StaticConfig {
|
||||
|
||||
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
ws.load_static_config(workspace_config, value.layout_defaults.as_ref())?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1651,7 +1715,10 @@ impl StaticConfig {
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
ws.load_static_config(
|
||||
workspace_config,
|
||||
value.layout_defaults.as_ref(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ use crate::animation::ANIMATION_MANAGER;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::animation::AnimationEngine;
|
||||
use crate::animation::GHOST_MOVEMENT_ENABLED;
|
||||
use crate::animation::RenderDispatcher;
|
||||
use crate::animation::ghost::GhostWindow;
|
||||
use crate::animation::lerp::Lerp;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::animation::prefix::new_animation_key;
|
||||
@@ -42,6 +44,7 @@ use crate::windows_api;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use color_eyre::eyre;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -52,6 +55,7 @@ use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
@@ -165,6 +169,18 @@ struct MovementRenderDispatcher {
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
/// Some between successful pre_render and post_render/cleanup_on_cancel when
|
||||
/// ghost movement is active. None for the legacy code path.
|
||||
ghost: Mutex<Option<GhostWindow>>,
|
||||
/// Tracks whether the source has been cloaked so cleanup can uncloak idempotently.
|
||||
cloaked: AtomicBool,
|
||||
/// Last lerped logical rect actually applied; used by cleanup_on_cancel to
|
||||
/// snap the real window to the position the user was last seeing.
|
||||
last_animated_rect: Mutex<Rect>,
|
||||
/// True when pre_render successfully repositioned the source to target_rect
|
||||
/// before registering the thumbnail. In that case post_render must skip
|
||||
/// the final position_window since the source is already there.
|
||||
pre_painted: AtomicBool,
|
||||
}
|
||||
|
||||
impl MovementRenderDispatcher {
|
||||
@@ -183,37 +199,33 @@ impl MovementRenderDispatcher {
|
||||
target_rect,
|
||||
top,
|
||||
style,
|
||||
ghost: Mutex::new(None),
|
||||
cloaked: AtomicBool::new(false),
|
||||
last_animated_rect: Mutex::new(start_rect),
|
||||
pre_painted: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for MovementRenderDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
|
||||
fn use_ghost(&self) -> bool {
|
||||
GHOST_MOVEMENT_ENABLED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> eyre::Result<()> {
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
Ok(())
|
||||
/// Chromium / Electron windows expose a top-level class beginning with
|
||||
/// `Chrome_WidgetWin_`. Their renderer pipeline is suspended whenever
|
||||
/// `NativeWindowOcclusionTrackerWin` reads any non-zero `DWMWA_CLOAKED`
|
||||
/// state on the HWND, so the pre-paint trick (cloak → SetWindowPos →
|
||||
/// capture) leaves the DComp swap chain stale and the post-uncloak frame
|
||||
/// shows half-painted / black regions. For these apps we fall back to
|
||||
/// capture-at-start: keep the source cloaked at start_rect for the whole
|
||||
/// animation and only move it to target in post_render, where the
|
||||
/// uncloak is the visibility flip that wakes Viz back up.
|
||||
fn source_is_chromium_shell(&self) -> bool {
|
||||
WindowsApi::real_window_class_w(self.hwnd)
|
||||
.map(|class| class.starts_with("Chrome_WidgetWin_"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> eyre::Result<()> {
|
||||
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
|
||||
|
||||
// we don't check WINDOW_HANDLING_BEHAVIOUR here because animations
|
||||
// are always run on a separate thread
|
||||
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
|
||||
WindowsApi::invalidate_rect(self.hwnd, None, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_render(&self) -> eyre::Result<()> {
|
||||
// we don't add the async_window_pos flag here because animations
|
||||
// are always run on a separate thread
|
||||
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)?;
|
||||
fn finalise_managers(&self) {
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX)
|
||||
@@ -228,9 +240,202 @@ impl RenderDispatcher for MovementRenderDispatcher {
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for MovementRenderDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> eyre::Result<()> {
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
if self.use_ghost() {
|
||||
let is_chromium = self.source_is_chromium_shell();
|
||||
|
||||
// The ghost host is sized to the LOGICAL rect (visible content
|
||||
// area). DWM thumbnails capture the source at its
|
||||
// DWMWA_EXTENDED_FRAME_BOUNDS extents (visible content), not
|
||||
// GetWindowRect outer extents that include the drop-shadow
|
||||
// margin. Sizing the host to outer dims would stretch the
|
||||
// visible-content texture by the shadow ratio.
|
||||
//
|
||||
// Place the ghost in z-order immediately above the source so
|
||||
// multiple simultaneously animating windows (workspace switches,
|
||||
// layout flips) keep the same relative stacking as their
|
||||
// sources rather than all piling up at HWND_TOP in creation
|
||||
// order.
|
||||
//
|
||||
// For non-Chromium sources we ALSO pre-position the source to
|
||||
// target_rect *before* registering the thumbnail, so the
|
||||
// captured pixels reflect target-dimensioned content. The ghost
|
||||
// dest then animates start → target with the texture
|
||||
// downscaling to native 1:1 at the end — crisp final frame
|
||||
// instead of an upscaled blur. For Chromium we skip pre-paint
|
||||
// (see `source_is_chromium_shell`).
|
||||
//
|
||||
// DwmSetWindowAttribute(DWMWA_CLOAK) is rejected with
|
||||
// E_ACCESSDENIED for foreign HWNDs; the undocumented
|
||||
// IApplicationView::SetCloak path used elsewhere does not have
|
||||
// that restriction.
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 2);
|
||||
self.cloaked.store(true, Ordering::SeqCst);
|
||||
|
||||
if !is_chromium {
|
||||
if let Err(error) =
|
||||
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)
|
||||
{
|
||||
tracing::warn!(
|
||||
"ghost movement: failed to pre-position hwnd {}: {error}",
|
||||
self.hwnd
|
||||
);
|
||||
} else {
|
||||
// No DwmFlush here. DWM thumbnails are live: once
|
||||
// registered, the thumbnail surface updates as the
|
||||
// source paints, so the texture catches up to
|
||||
// target-dim content within the first frame or two of
|
||||
// the animation. Skipping the flush avoids a ~16ms
|
||||
// pre-render stall on every non-Chromium animation.
|
||||
self.pre_painted.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
match GhostWindow::create(self.hwnd, self.start_rect, Some(self.hwnd)) {
|
||||
Ok(ghost) => {
|
||||
*self.ghost.lock() = Some(ghost);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"ghost movement: failed to create ghost for hwnd {}: {error}; \
|
||||
uncloaking and falling back to legacy path",
|
||||
self.hwnd
|
||||
);
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
|
||||
self.cloaked.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> eyre::Result<()> {
|
||||
let logical = self.start_rect.lerp(self.target_rect, progress, self.style);
|
||||
*self.last_animated_rect.lock() = logical;
|
||||
|
||||
let ghost_active = self.ghost.lock().is_some();
|
||||
if ghost_active {
|
||||
if let Some(ghost) = self.ghost.lock().as_ref()
|
||||
&& let Err(error) = ghost.update_rect(logical)
|
||||
{
|
||||
tracing::trace!("ghost update_rect failed: {error}");
|
||||
}
|
||||
border_manager::animate_to(self.hwnd, logical);
|
||||
} else {
|
||||
// Legacy path: animations always run on a separate thread, so we don't
|
||||
// gate on WINDOW_HANDLING_BEHAVIOUR here.
|
||||
WindowsApi::move_window(self.hwnd, &logical, false)?;
|
||||
WindowsApi::invalidate_rect(self.hwnd, None, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_render(&self) -> eyre::Result<()> {
|
||||
let used_ghost = self.ghost.lock().is_some();
|
||||
let pre_painted = self.pre_painted.load(Ordering::SeqCst);
|
||||
|
||||
// Final single SetWindowPos. For the pre-paint ghost path the source
|
||||
// has already been moved to target_rect in pre_render and we skip
|
||||
// this. For the Chromium ghost path (no pre-paint) the source is
|
||||
// still cloaked at start_rect and needs to be moved here. For the
|
||||
// legacy non-ghost path this is the original final reposition.
|
||||
if !pre_painted {
|
||||
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)?;
|
||||
}
|
||||
|
||||
// Uncloak BEFORE crossfade so the real window's first post-resize
|
||||
// frame is being composed underneath the still-visible ghost while
|
||||
// we fade. This gives Chromium/Electron renderers time to produce a
|
||||
// CompositorFrame at the new size — the visibility flip from
|
||||
// cloaked-to-uncloaked is what nudges Viz to resume frame
|
||||
// production.
|
||||
if self.cloaked.swap(false, Ordering::SeqCst) {
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
|
||||
}
|
||||
|
||||
if used_ghost {
|
||||
// Crossfade the ghost out over several DWM frames. This masks the
|
||||
// texture mismatch (start-dim bitmap stretched vs. crisp
|
||||
// target-dim repaint) and gives slow-to-repaint apps time to
|
||||
// present their first post-resize frame before the overlay is
|
||||
// removed. Mirrors KWin's geometry-effect crossfade.
|
||||
//
|
||||
// Ease-in curve (1 - t^3): opacity holds high for most of the
|
||||
// fade and only drops sharply at the end. The ghost stays
|
||||
// prominent while the real window's first few frames land
|
||||
// underneath, so the user perceives a smooth reveal rather than
|
||||
// a snap.
|
||||
//
|
||||
// We call set_opacity directly (synchronous DwmUpdateThumbnailProperties
|
||||
// on this thread) rather than via the ghost owner channel, so
|
||||
// each step is guaranteed to be visible before the following
|
||||
// DwmFlush waits for the next vblank.
|
||||
if let Some(ghost) = self.ghost.lock().as_ref() {
|
||||
const FADE_STEPS: u32 = 8;
|
||||
for step in 1..=FADE_STEPS {
|
||||
let t = step as f32 / FADE_STEPS as f32;
|
||||
let progress = t * t * t;
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
let opacity_u8 = ((1.0 - progress) * 255.0).round().clamp(0.0, 255.0) as u8;
|
||||
let _ = ghost.set_opacity(opacity_u8);
|
||||
unsafe {
|
||||
let _ = windows::Win32::Graphics::Dwm::DwmFlush();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy path: still benefit from one DWM frame's wait so the
|
||||
// app's first post-move paint lands.
|
||||
unsafe {
|
||||
let _ = windows::Win32::Graphics::Dwm::DwmFlush();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ghost) = self.ghost.lock().take() {
|
||||
let _ = ghost.dispose();
|
||||
}
|
||||
|
||||
self.finalise_managers();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup_on_cancel(&self) {
|
||||
// Snap the real window to wherever the ghost was last drawn so the next
|
||||
// dispatcher can capture an accurate start_rect. Then uncloak and tear
|
||||
// down the ghost. Mirrors post_render but uses last_animated_rect.
|
||||
let target = *self.last_animated_rect.lock();
|
||||
|
||||
if let Err(error) = WindowsApi::position_window(self.hwnd, &target, false, false) {
|
||||
tracing::warn!(
|
||||
"ghost movement cancel: failed to snap hwnd {} to last rect: {error}",
|
||||
self.hwnd
|
||||
);
|
||||
}
|
||||
|
||||
if self.cloaked.swap(false, Ordering::SeqCst) {
|
||||
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
|
||||
}
|
||||
|
||||
if let Some(ghost) = self.ghost.lock().take() {
|
||||
let _ = ghost.dispose();
|
||||
}
|
||||
|
||||
self.finalise_managers();
|
||||
}
|
||||
}
|
||||
|
||||
struct TransparencyRenderDispatcher {
|
||||
|
||||
@@ -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()?;
|
||||
@@ -3341,7 +3376,7 @@ impl WindowManager {
|
||||
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
|
||||
rules.retain(|pair| pair.0 != at_container_count);
|
||||
rules.push((at_container_count, Layout::Default(layout)));
|
||||
rules.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
rules.sort_by_key(|a| a.0);
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
@@ -3384,7 +3419,7 @@ impl WindowManager {
|
||||
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
|
||||
rules.retain(|pair| pair.0 != at_container_count);
|
||||
rules.push((at_container_count, Layout::Custom(layout)));
|
||||
rules.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
rules.sort_by_key(|a| a.0);
|
||||
|
||||
// If this is the focused workspace on a non-focused screen, let's update it
|
||||
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
|
||||
|
||||
@@ -24,6 +24,7 @@ use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
|
||||
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
|
||||
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
|
||||
use windows::Win32::Graphics::Dwm::DWM_THUMBNAIL_PROPERTIES;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_BORDER_COLOR;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_COLOR_NONE;
|
||||
@@ -32,7 +33,10 @@ use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
|
||||
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
|
||||
use windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
|
||||
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DwmRegisterThumbnail;
|
||||
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DwmUnregisterThumbnail;
|
||||
use windows::Win32::Graphics::Dwm::DwmUpdateThumbnailProperties;
|
||||
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
||||
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
|
||||
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
|
||||
@@ -144,6 +148,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
|
||||
@@ -1343,6 +1348,41 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_ghost_host_window(name: PCWSTR, instance: isize) -> eyre::Result<isize> {
|
||||
unsafe {
|
||||
CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT,
|
||||
name,
|
||||
name,
|
||||
WS_POPUP,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
None,
|
||||
None,
|
||||
Option::from(HINSTANCE(as_ptr!(instance))),
|
||||
None,
|
||||
)?
|
||||
}
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn dwm_register_thumbnail(dest_hwnd: isize, src_hwnd: isize) -> eyre::Result<isize> {
|
||||
Ok(unsafe { DwmRegisterThumbnail(HWND(as_ptr!(dest_hwnd)), HWND(as_ptr!(src_hwnd))) }?)
|
||||
}
|
||||
|
||||
pub fn dwm_update_thumbnail_properties(
|
||||
hthumb: isize,
|
||||
props: &DWM_THUMBNAIL_PROPERTIES,
|
||||
) -> eyre::Result<()> {
|
||||
unsafe { DwmUpdateThumbnailProperties(hthumb, props) }.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn dwm_unregister_thumbnail(hthumb: isize) -> eyre::Result<()> {
|
||||
unsafe { DwmUnregisterThumbnail(hthumb) }.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> eyre::Result<isize> {
|
||||
unsafe {
|
||||
CreateWindowExW(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Display;
|
||||
@@ -25,6 +26,7 @@ use crate::core::CustomLayout;
|
||||
use crate::core::CycleDirection;
|
||||
use crate::core::DefaultLayout;
|
||||
use crate::core::Layout;
|
||||
use crate::core::LayoutDefaultEntry;
|
||||
use crate::core::LayoutOptions;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
@@ -61,6 +63,15 @@ pub struct Workspace {
|
||||
pub layout: Layout,
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
pub layout_rules: Vec<(usize, Layout)>,
|
||||
/// Threshold-based layout options rules (container_count >= threshold -> use these options).
|
||||
/// Sorted by threshold ascending at load time.
|
||||
#[serde(default)]
|
||||
pub layout_options_rules: Vec<(usize, LayoutOptions)>,
|
||||
/// Cached per-layout defaults from the global `layout_defaults` config setting.
|
||||
/// Pre-sorted at config load time; used as fallback when workspace has no overrides.
|
||||
#[serde(skip)]
|
||||
pub(crate) layout_defaults_cache: HashMap<DefaultLayout, CachedLayoutDefault>,
|
||||
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 +129,9 @@ impl Default for Workspace {
|
||||
layout: Layout::Default(DefaultLayout::BSP),
|
||||
layout_options: None,
|
||||
layout_rules: vec![],
|
||||
layout_options_rules: vec![],
|
||||
layout_defaults_cache: HashMap::new(),
|
||||
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)),
|
||||
@@ -163,8 +177,49 @@ pub struct WorkspaceGlobals {
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
/// Cached per-layout default options (pre-sorted rules) derived from the global `layout_defaults`.
|
||||
pub(crate) struct CachedLayoutDefault {
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
/// Threshold-based rules, sorted by threshold ascending at load time
|
||||
pub layout_options_rules: Vec<(usize, LayoutOptions)>,
|
||||
}
|
||||
|
||||
/// Convert an optional HashMap of threshold-based layout options rules into a Vec sorted by
|
||||
/// threshold ascending.
|
||||
fn sorted_layout_options_rules(
|
||||
rules: Option<&HashMap<usize, LayoutOptions>>,
|
||||
) -> Vec<(usize, LayoutOptions)> {
|
||||
match rules {
|
||||
Some(rules) => {
|
||||
let mut sorted: Vec<(usize, LayoutOptions)> =
|
||||
rules.iter().map(|(t, o)| (*t, *o)).collect();
|
||||
sorted.sort_by_key(|(t, _)| *t);
|
||||
sorted
|
||||
}
|
||||
None => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the highest matching threshold rule for the given container count.
|
||||
/// Rules must be sorted by threshold ascending.
|
||||
fn resolve_threshold_match(
|
||||
rules: &[(usize, LayoutOptions)],
|
||||
container_count: usize,
|
||||
) -> Option<LayoutOptions> {
|
||||
rules
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(threshold, _)| container_count >= *threshold)
|
||||
.map(|(_, opts)| *opts)
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> eyre::Result<()> {
|
||||
pub fn load_static_config(
|
||||
&mut self,
|
||||
config: &WorkspaceConfig,
|
||||
layout_defaults: Option<&HashMap<DefaultLayout, LayoutDefaultEntry>>,
|
||||
) -> eyre::Result<()> {
|
||||
self.name = Option::from(config.name.clone());
|
||||
|
||||
self.container_padding = config.container_padding;
|
||||
@@ -213,6 +268,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 =
|
||||
@@ -240,19 +304,78 @@ impl Workspace {
|
||||
self.layout_flip = config.layout_flip;
|
||||
self.floating_layer_behaviour = config.floating_layer_behaviour;
|
||||
self.wallpaper = config.wallpaper.clone();
|
||||
|
||||
// Load layout options directly (LayoutOptions is used in both config and runtime)
|
||||
self.layout_options = config.layout_options;
|
||||
|
||||
// Load threshold-based layout options rules, sorted by threshold ascending
|
||||
self.layout_options_rules =
|
||||
sorted_layout_options_rules(config.layout_options_rules.as_ref());
|
||||
|
||||
tracing::debug!(
|
||||
"Workspace '{}' loaded layout_options: {:?}",
|
||||
"Workspace '{}' loaded layout_options: {:?}, layout_options_rules: {} entries",
|
||||
self.name.as_deref().unwrap_or("unnamed"),
|
||||
self.layout_options
|
||||
self.layout_options,
|
||||
self.layout_options_rules.len(),
|
||||
);
|
||||
|
||||
// Cache per-layout defaults from global layout_defaults, pre-sorting rules
|
||||
self.layout_defaults_cache = if let Some(defaults) = layout_defaults {
|
||||
defaults
|
||||
.iter()
|
||||
.map(|(layout, entry)| {
|
||||
(
|
||||
*layout,
|
||||
CachedLayoutDefault {
|
||||
layout_options: entry.layout_options,
|
||||
layout_options_rules: sorted_layout_options_rules(
|
||||
entry.layout_options_rules.as_ref(),
|
||||
),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
self.workspace_config = Some(config.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute effective layout options using the complete-replacement cascade:
|
||||
///
|
||||
/// If the workspace defines EITHER `layout_options` OR `layout_options_rules`,
|
||||
/// it completely replaces the global `layout_defaults` for this layout.
|
||||
/// Global defaults are only used when the workspace has NEITHER setting.
|
||||
///
|
||||
/// Within the effective source (workspace or global):
|
||||
/// 1. Try threshold match from rules (highest matching threshold wins)
|
||||
/// 2. If a rule matches -> use it (full replacement of base)
|
||||
/// 3. Else -> use the base `layout_options`
|
||||
fn effective_layout_options(&self) -> Option<LayoutOptions> {
|
||||
let container_count = self.containers().len();
|
||||
|
||||
let has_workspace_overrides =
|
||||
self.layout_options.is_some() || !self.layout_options_rules.is_empty();
|
||||
|
||||
let (effective_base, effective_rules): (Option<LayoutOptions>, &[(usize, LayoutOptions)]) =
|
||||
if has_workspace_overrides {
|
||||
(self.layout_options, &self.layout_options_rules)
|
||||
} else {
|
||||
match &self.layout {
|
||||
Layout::Default(dl) => match self.layout_defaults_cache.get(dl) {
|
||||
Some(entry) => (entry.layout_options, &entry.layout_options_rules),
|
||||
None => (None, &[]),
|
||||
},
|
||||
Layout::Custom(_) => (None, &[]),
|
||||
}
|
||||
};
|
||||
|
||||
resolve_threshold_match(effective_rules, container_count).or(effective_base)
|
||||
}
|
||||
|
||||
pub fn hide(&mut self, omit: Option<isize>) {
|
||||
for window in self.floating_windows_mut().iter_mut().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
@@ -479,9 +602,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 +636,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
|
||||
@@ -556,10 +696,14 @@ impl Workspace {
|
||||
} else if let Some(window) = &mut self.maximized_window {
|
||||
window.maximize();
|
||||
} else if !self.containers().is_empty() {
|
||||
let effective_layout_options = self.effective_layout_options();
|
||||
|
||||
tracing::debug!(
|
||||
"Workspace '{}' update() - self.layout_options before calculate: {:?}",
|
||||
"Workspace '{}' update() - effective_layout_options: {:?} (base: {:?}, rules: {})",
|
||||
self.name.as_deref().unwrap_or("unnamed"),
|
||||
self.layout_options
|
||||
effective_layout_options,
|
||||
self.layout_options,
|
||||
self.layout_options_rules.len(),
|
||||
);
|
||||
let mut layouts = self.layout.as_boxed_arrangement().calculate(
|
||||
&adjusted_work_area,
|
||||
@@ -570,7 +714,7 @@ impl Workspace {
|
||||
self.layout_flip,
|
||||
&self.resize_dimensions,
|
||||
self.focused_container_idx(),
|
||||
self.layout_options,
|
||||
effective_layout_options,
|
||||
&self.latest_layout,
|
||||
);
|
||||
|
||||
@@ -1515,6 +1659,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();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.40"
|
||||
version = "0.1.42"
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
|
||||
@@ -1924,13 +1924,11 @@ fn main() -> eyre::Result<()> {
|
||||
"Application specific configuration file path has not been set. Try running 'komorebic fetch-asc'\n"
|
||||
);
|
||||
}
|
||||
Some(AppSpecificConfigurationPath::Single(path)) => {
|
||||
if !path.exists() {
|
||||
println!(
|
||||
"Application specific configuration file path '{}' does not exist. Try running 'komorebic fetch-asc'\n",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Some(AppSpecificConfigurationPath::Single(path)) if !path.exists() => {
|
||||
println!(
|
||||
"Application specific configuration file path '{}' does not exist. Try running 'komorebic fetch-asc'\n",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
1656
schema.bar.json
1656
schema.bar.json
File diff suppressed because it is too large
Load Diff
78
schema.json
78
schema.json
@@ -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.42`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
@@ -304,6 +304,16 @@
|
||||
"$ref": "#/$defs/MatchingRule"
|
||||
}
|
||||
},
|
||||
"layout_defaults": {
|
||||
"description": "Per-layout default options and rules, keyed by layout name.\nApplied as fallback when a workspace does not define its own layout_options or layout_options_rules.\nIf a workspace defines either setting, all global defaults for that layout are completely replaced.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/LayoutDefaultEntry"
|
||||
}
|
||||
},
|
||||
"manage_rules": {
|
||||
"description": "Individual window force-manage rules",
|
||||
"type": [
|
||||
@@ -703,7 +713,7 @@
|
||||
},
|
||||
{
|
||||
"title": "CubicBezier",
|
||||
"description": "Custom Cubic Bézier function",
|
||||
"description": "Custom Cubic Bezier function",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"CubicBezier": {
|
||||
@@ -768,6 +778,14 @@
|
||||
"default": 60,
|
||||
"minimum": 0
|
||||
},
|
||||
"ghost_movement": {
|
||||
"description": "Render movement animations on a GPU-composited ghost surface (recommended).\nWhen false, falls back to the legacy per-frame MoveWindow path.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"default": true
|
||||
},
|
||||
"style": {
|
||||
"description": "Set the animation style",
|
||||
"anyOf": [
|
||||
@@ -3290,6 +3308,36 @@
|
||||
"colours"
|
||||
]
|
||||
},
|
||||
"LayoutDefaultEntry": {
|
||||
"description": "Per-layout default options entry for the `layout_defaults` global setting.\nContains both base layout options and threshold-based layout options rules.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"layout_options": {
|
||||
"description": "Default layout options for this layout",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/LayoutOptions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout_options_rules": {
|
||||
"description": "Threshold-based layout options rules in the format of threshold => options.\nWhen container count >= threshold, the highest matching threshold's options\nfully replace the base `layout_options`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^\\d+$": {
|
||||
"$ref": "#/$defs/LayoutOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"LayoutOptions": {
|
||||
"description": "Options for specific layouts",
|
||||
"type": "object",
|
||||
@@ -4214,6 +4262,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout_options_rules": {
|
||||
"description": "Threshold-based layout options rules in the format of threshold => options.\nWhen container count >= threshold, the highest matching threshold's options\nfully replace the base `layout_options`.\nThis follows the same threshold logic as `layout_rules`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^\\d+$": {
|
||||
"$ref": "#/$defs/LayoutOptions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout_rules": {
|
||||
"description": "Layout rules in the format of threshold => layout",
|
||||
"type": [
|
||||
@@ -4286,6 +4347,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": [
|
||||
|
||||
Reference in New Issue
Block a user